Compare commits
247 Commits
@vercel/py
...
@vercel/ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32afd67d29 | ||
|
|
7523e39f18 | ||
|
|
99f2f2f1ba | ||
|
|
63830d38ce | ||
|
|
f3428dd212 | ||
|
|
5eb8b16cbd | ||
|
|
226bf02be2 | ||
|
|
8505872f55 | ||
|
|
7db6436797 | ||
|
|
e2d76e9c92 | ||
|
|
337cb21d67 | ||
|
|
6bfff3e9eb | ||
|
|
ac5b259c11 | ||
|
|
bfc553db11 | ||
|
|
2b101d4692 | ||
|
|
3316f38cb4 | ||
|
|
7837387127 | ||
|
|
f478200dd3 | ||
|
|
c29de8206a | ||
|
|
a2df3b5463 | ||
|
|
73446e544a | ||
|
|
21ff4a58c3 | ||
|
|
2b9eb02b8c | ||
|
|
4ef4722460 | ||
|
|
be5308b137 | ||
|
|
08a83a94f8 | ||
|
|
543ffdfe5c | ||
|
|
c11527e904 | ||
|
|
d296064386 | ||
|
|
400a6c42bd | ||
|
|
71b3ded398 | ||
|
|
fc3fa61b59 | ||
|
|
1f98c4fee7 | ||
|
|
1cb5a91727 | ||
|
|
e8c7db59cf | ||
|
|
57b230e25f | ||
|
|
ab3fb25790 | ||
|
|
88d98f7497 | ||
|
|
90c1895949 | ||
|
|
46a1f3670b | ||
|
|
4b025fee92 | ||
|
|
dc8293dc13 | ||
|
|
78883dea23 | ||
|
|
b5b792e42f | ||
|
|
8993a3c4af | ||
|
|
57241aad81 | ||
|
|
4773ff5efd | ||
|
|
d8c7308eb6 | ||
|
|
5df1c89138 | ||
|
|
f5d879143c | ||
|
|
9a55809515 | ||
|
|
56adf15823 | ||
|
|
1acab3d06c | ||
|
|
081b38466b | ||
|
|
c397fd1856 | ||
|
|
afd303b94a | ||
|
|
b12387034a | ||
|
|
5af65d5a24 | ||
|
|
1ee9a96a62 | ||
|
|
76130faf26 | ||
|
|
fb3601d178 | ||
|
|
aebfb6812d | ||
|
|
73999e7253 | ||
|
|
989dad5570 | ||
|
|
68c2dea601 | ||
|
|
63f2da2f68 | ||
|
|
57e5f81361 | ||
|
|
fd5e440533 | ||
|
|
2a45805b26 | ||
|
|
5523383e50 | ||
|
|
0ecbb24cab | ||
|
|
922223bd19 | ||
|
|
0ad7fd34f4 | ||
|
|
3d3774ee7e | ||
|
|
50f8eec7cb | ||
|
|
45374e2f90 | ||
|
|
fd9142b6f3 | ||
|
|
8cf67b549b | ||
|
|
5dc6f48e44 | ||
|
|
66c8544e8f | ||
|
|
0140db38fa | ||
|
|
e5421c27e8 | ||
|
|
5afc527233 | ||
|
|
de9518b010 | ||
|
|
c322d1dbba | ||
|
|
18c19ead76 | ||
|
|
9d80c27382 | ||
|
|
bef1aec766 | ||
|
|
4f4a42813f | ||
|
|
181a492d91 | ||
|
|
1be7a80bb8 | ||
|
|
0428d4744e | ||
|
|
2a929a4bb9 | ||
|
|
accd308dc5 | ||
|
|
e2d4efab08 | ||
|
|
7e0dd6f808 | ||
|
|
8971e02e49 | ||
|
|
10c91c8579 | ||
|
|
bfdbe58675 | ||
|
|
7bdaf107b7 | ||
|
|
8de100f0e1 | ||
|
|
38a6785859 | ||
|
|
c67d1a8525 | ||
|
|
c5a7c574a2 | ||
|
|
d2f8d178f7 | ||
|
|
f9a747764c | ||
|
|
27d80f13cd | ||
|
|
8c668c925d | ||
|
|
4b1b33c143 | ||
|
|
a8d4147554 | ||
|
|
09339f494d | ||
|
|
ee4d772ae9 | ||
|
|
61e8103404 | ||
|
|
fb4f477325 | ||
|
|
016bff848e | ||
|
|
183e411f7c | ||
|
|
070e300148 | ||
|
|
cbdf9b4a88 | ||
|
|
ec9b55dc81 | ||
|
|
06829bc21a | ||
|
|
628071f659 | ||
|
|
5a7461dfe3 | ||
|
|
599f8f675c | ||
|
|
0a8bc494fc | ||
|
|
34e008f42e | ||
|
|
037633b3f1 | ||
|
|
1a6f3c0270 | ||
|
|
0a2af4fb94 | ||
|
|
3fb1c50142 | ||
|
|
de033c43fd | ||
|
|
f8e5df749c | ||
|
|
5670acc2cc | ||
|
|
5205047851 | ||
|
|
1edc2d06c9 | ||
|
|
fdb15b2539 | ||
|
|
32ebcd83a7 | ||
|
|
2e43b2b88a | ||
|
|
f83d432fcd | ||
|
|
87fc38e860 | ||
|
|
afc4388fc0 | ||
|
|
3c48b40b43 | ||
|
|
ce89f00328 | ||
|
|
621b53bc49 | ||
|
|
728b620355 | ||
|
|
7d16395038 | ||
|
|
59e1259688 | ||
|
|
169242157e | ||
|
|
db10ffd679 | ||
|
|
c0d0744c4e | ||
|
|
9da67423a5 | ||
|
|
51fe09d5e9 | ||
|
|
695bfbdd60 | ||
|
|
547e88228e | ||
|
|
9bfb5dd535 | ||
|
|
81ea84fae8 | ||
|
|
fa8bf07be4 | ||
|
|
cc9dce73ad | ||
|
|
bba7cbd411 | ||
|
|
9a3739bebd | ||
|
|
8c62de16ce | ||
|
|
e9333988d7 | ||
|
|
fb001ce7eb | ||
|
|
b399fe7037 | ||
|
|
88385b3c84 | ||
|
|
eed39913e1 | ||
|
|
03e9047bc9 | ||
|
|
0e35205bf1 | ||
|
|
e42fe34c4a | ||
|
|
3ece7ac969 | ||
|
|
4f832acf90 | ||
|
|
918726e01d | ||
|
|
dc2ddf867b | ||
|
|
ee1211416f | ||
|
|
570fd24e29 | ||
|
|
40681ad0f4 | ||
|
|
f20703b15d | ||
|
|
68eb197112 | ||
|
|
b8b87b96da | ||
|
|
967c24f1bb | ||
|
|
609f781234 | ||
|
|
998f6bf6e6 | ||
|
|
7511c2ef85 | ||
|
|
71425fac1f | ||
|
|
6973cd5989 | ||
|
|
24785ff50a | ||
|
|
aa3ad4478c | ||
|
|
f0d73049ca | ||
|
|
6cef07354a | ||
|
|
50af9f5b75 | ||
|
|
af76b134d8 | ||
|
|
c7640005fd | ||
|
|
3deed977ba | ||
|
|
b38c360e36 | ||
|
|
1595e48414 | ||
|
|
e6b0ee3e3c | ||
|
|
a247e31688 | ||
|
|
dc02e763a4 | ||
|
|
8567fc0de6 | ||
|
|
4f8f3d373f | ||
|
|
debb85b690 | ||
|
|
bfef989ada | ||
|
|
4e0b6c5eaf | ||
|
|
0ace69ef75 | ||
|
|
b7b7923f92 | ||
|
|
8167233c56 | ||
|
|
32ee6aba92 | ||
|
|
b48f7a7e6e | ||
|
|
a961c9b992 | ||
|
|
cf7c50d691 | ||
|
|
f4be388a1f | ||
|
|
c1bc53dea8 | ||
|
|
6855e3df54 | ||
|
|
0d39dbd1d9 | ||
|
|
509c85182a | ||
|
|
ae801e563d | ||
|
|
0e8278f490 | ||
|
|
0d302a6f48 | ||
|
|
4e4f5f28a2 | ||
|
|
5205a4ec4b | ||
|
|
2c15e496ed | ||
|
|
1f0ca46626 | ||
|
|
17cb5f1bc6 | ||
|
|
b095031292 | ||
|
|
f50bcbc0ba | ||
|
|
4bf6295d7a | ||
|
|
a4001ce10b | ||
|
|
2df3432d88 | ||
|
|
bcfc19de12 | ||
|
|
04381c669b | ||
|
|
0c7b54edad | ||
|
|
6d42816395 | ||
|
|
6fe6d05a42 | ||
|
|
50a201f145 | ||
|
|
701a02ae9d | ||
|
|
39f7586621 | ||
|
|
c4a39c8d29 | ||
|
|
3ac238cf08 | ||
|
|
8384813a0d | ||
|
|
c4587de439 | ||
|
|
d997dc4fbc | ||
|
|
d15b90bd4d | ||
|
|
5b31297f0c | ||
|
|
e232566cbe | ||
|
|
592689cad1 | ||
|
|
9b08e72f76 | ||
|
|
bd0e10cfe7 | ||
|
|
28436ade60 |
@@ -19,6 +19,9 @@ packages/cli/src/util/dev/templates/*.ts
|
||||
packages/client/tests/fixtures
|
||||
packages/client/lib
|
||||
|
||||
# hydrogen
|
||||
packages/hydrogen/edge-entry.js
|
||||
|
||||
# next
|
||||
packages/next/test/integration/middleware
|
||||
packages/next/test/integration/middleware-eval
|
||||
|
||||
4
.gitattributes
vendored
@@ -8,3 +8,7 @@ packages/*/test/* linguist-vendored
|
||||
# Go build fails with Windows line endings.
|
||||
*.go text eol=lf
|
||||
go.mod text eol=lf
|
||||
|
||||
# Mark certain files as "binary" -- hide diffs
|
||||
**/test/fixtures/**/git/**/* binary
|
||||
**/test/fixtures/**/git/**/* linguist-generated
|
||||
|
||||
1
.github/CONTRIBUTING.md
vendored
@@ -12,6 +12,7 @@ To get started, execute the following:
|
||||
|
||||
```
|
||||
git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
yarn install
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
|
||||
10
.github/workflows/test.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tests: ${{ steps['set-tests'].outputs['tests'] }}
|
||||
dplUrl: ${{ steps.waitForTarball.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: git --version
|
||||
@@ -32,6 +33,12 @@ jobs:
|
||||
echo "Files to test:"
|
||||
echo "$TESTS_ARRAY"
|
||||
echo "::set-output name=tests::$TESTS_ARRAY"
|
||||
- uses: patrickedqvist/wait-for-vercel-preview@ae34b392ef30297f2b672f9afb3c329bde9bd487
|
||||
id: waitForTarball
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
max_timeout: 360
|
||||
check_interval: 5
|
||||
|
||||
test:
|
||||
timeout-minutes: 120
|
||||
@@ -69,13 +76,14 @@ jobs:
|
||||
- run: yarn install --network-timeout 1000000
|
||||
|
||||
- name: Build ${{matrix.packageName}} and all its dependencies
|
||||
run: yarn turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
run: node_modules/.bin/turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
env:
|
||||
FORCE_COLOR: '1'
|
||||
- name: Test ${{matrix.packageName}}
|
||||
run: node_modules/.bin/turbo run test --cache-dir=".turbo" --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_CLI_VERSION: ${{ needs.setup.outputs.dplUrl }}/tarballs/vercel.tgz
|
||||
VERCEL_TEAM_TOKEN: ${{ secrets.VERCEL_TEAM_TOKEN }}
|
||||
VERCEL_REGISTRATION_URL: ${{ secrets.VERCEL_REGISTRATION_URL }}
|
||||
FORCE_COLOR: '1'
|
||||
|
||||
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
# https://prettier.io/docs/en/ignore.html
|
||||
|
||||
# ignore these files with an intentional syntax error
|
||||
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js
|
||||
packages/cli/test/fixtures/unit/commands/build/node-error/api/typescript.ts
|
||||
@@ -1,18 +1 @@
|
||||
*
|
||||
|
||||
# general
|
||||
!utils/
|
||||
!utils/run.js
|
||||
!.yarnrc
|
||||
!yarn.lock
|
||||
!package.json
|
||||
!turbo.json
|
||||
|
||||
# api
|
||||
!api/
|
||||
!api/**
|
||||
|
||||
# packages
|
||||
!packages/
|
||||
!packages/frameworks
|
||||
!packages/frameworks/**
|
||||
packages/*/test/**
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Runtime Developer Reference
|
||||
|
||||
The following page is a reference for how to create a Runtime by implementing
|
||||
the Runtime API interface.
|
||||
the Runtime API interface. It's a way to add support for a new programming language to Vercel.
|
||||
|
||||
> Note: If you're the author of a web framework, please use the [Build Output API](https://vercel.com/docs/build-output-api/v3) instead to make your framework compatible with Vercel.
|
||||
|
||||
A Runtime is an npm module that implements the following interface:
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { join, dirname } from 'path';
|
||||
import execa from 'execa';
|
||||
import { getExampleList } from '../examples/example-list';
|
||||
import { mapOldToNew } from '../examples/map-old-to-new';
|
||||
|
||||
@@ -40,7 +41,32 @@ async function main() {
|
||||
JSON.stringify([...existingExamples, ...oldExamples])
|
||||
);
|
||||
|
||||
const { stdout: sha } = await execa('git', ['rev-parse', '--short', 'HEAD'], {
|
||||
cwd: repoRoot,
|
||||
});
|
||||
|
||||
const tarballsDir = join(pubDir, 'tarballs');
|
||||
const packagesDir = join(repoRoot, 'packages');
|
||||
const packages = await fs.readdir(packagesDir);
|
||||
for (const pkg of packages) {
|
||||
const fullDir = join(packagesDir, pkg);
|
||||
const packageJsonRaw = await fs.readFile(
|
||||
join(fullDir, 'package.json'),
|
||||
'utf-8'
|
||||
);
|
||||
const packageJson = JSON.parse(packageJsonRaw);
|
||||
const tarballName = `${packageJson.name
|
||||
.replace('@', '')
|
||||
.replace('/', '-')}-v${packageJson.version}-${sha.trim()}.tgz`;
|
||||
const destTarballPath = join(tarballsDir, `${packageJson.name}.tgz`);
|
||||
await fs.mkdir(dirname(destTarballPath), { recursive: true });
|
||||
await fs.copyFile(join(fullDir, tarballName), destTarballPath);
|
||||
}
|
||||
|
||||
console.log('Completed building static frontend.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch(err => {
|
||||
console.log('error running build:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
"description": "API for the vercel/vercel repo",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"vercel-build": "node ../utils/run.js build all"
|
||||
"//TODO": "We should add this pkg to yarn workspaces"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "5.11.1",
|
||||
"got": "10.2.1",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"parse-github-url": "1.0.2",
|
||||
"tar-fs": "2.0.0",
|
||||
"unzip-stream": "0.3.0"
|
||||
|
||||
2
examples/create-react-app/.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
# `REACT_APP` prefix is required to expose to client-side
|
||||
REACT_APP_VERCEL_ANALYTICS_ID=$VERCEL_ANALYTICS_ID
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"web-vitals": "^2.1.3"
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^14.2.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { sendToVercelAnalytics } from './vitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
@@ -11,7 +12,4 @@ ReactDOM.render(
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
reportWebVitals(sendToVercelAnalytics);
|
||||
|
||||
40
examples/create-react-app/src/vitals.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
|
||||
|
||||
function getConnectionSpeed() {
|
||||
return 'connection' in navigator &&
|
||||
navigator['connection'] &&
|
||||
'effectiveType' in navigator['connection']
|
||||
? navigator['connection']['effectiveType']
|
||||
: '';
|
||||
}
|
||||
|
||||
export function sendToVercelAnalytics(metric) {
|
||||
const analyticsId = process.env.REACT_APP_VERCEL_ANALYTICS_ID;
|
||||
if (!analyticsId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
dsn: analyticsId,
|
||||
id: metric.id,
|
||||
page: window.location.pathname,
|
||||
href: window.location.href,
|
||||
event_name: metric.name,
|
||||
value: metric.value.toString(),
|
||||
speed: getConnectionSpeed(),
|
||||
};
|
||||
|
||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||
// This content type is necessary for `sendBeacon`
|
||||
type: 'application/x-www-form-urlencoded',
|
||||
});
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(vitalsUrl, blob);
|
||||
} else
|
||||
fetch(vitalsUrl, {
|
||||
body: blob,
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
keepalive: true,
|
||||
});
|
||||
}
|
||||
@@ -1484,10 +1484,10 @@
|
||||
"@svgr/plugin-svgo" "^5.5.0"
|
||||
loader-utils "^2.0.0"
|
||||
|
||||
"@testing-library/dom@^8.0.0":
|
||||
version "8.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.2.tgz#fc110c665a066c2287be765e4a35ba8dad737015"
|
||||
integrity sha512-idsS/cqbYudXcVWngc1PuWNmXs416oBy2g/7Q8QAUREt5Z3MUkAL2XJD7xazLJ6esDfqRDi/ZBxk+OPPXitHRw==
|
||||
"@testing-library/dom@^8.5.0":
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5"
|
||||
integrity sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
@@ -1498,10 +1498,10 @@
|
||||
lz-string "^1.4.4"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/jest-dom@^5.16.1":
|
||||
version "5.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f"
|
||||
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==
|
||||
"@testing-library/jest-dom@^5.16.4":
|
||||
version "5.16.4"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd"
|
||||
integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
"@types/testing-library__jest-dom" "^5.9.1"
|
||||
@@ -1513,20 +1513,19 @@
|
||||
lodash "^4.17.15"
|
||||
redent "^3.0.0"
|
||||
|
||||
"@testing-library/react@^12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76"
|
||||
integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==
|
||||
"@testing-library/react@^13.3.0":
|
||||
version "13.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.3.0.tgz#bf298bfbc5589326bbcc8052b211f3bb097a97c5"
|
||||
integrity sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@testing-library/dom" "^8.0.0"
|
||||
"@testing-library/dom" "^8.5.0"
|
||||
"@types/react-dom" "^18.0.0"
|
||||
|
||||
"@testing-library/user-event@^13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
|
||||
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@testing-library/user-event@^14.2.0":
|
||||
version "14.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.2.0.tgz#8293560f8f80a00383d6c755ec3e0b918acb1683"
|
||||
integrity sha512-+hIlG4nJS6ivZrKnOP7OGsDu9Fxmryj9vCl8x0ZINtTJcCHs2zLsYif5GzuRiBF2ck5GZG2aQr7Msg+EHlnYVQ==
|
||||
|
||||
"@tootallnate/once@1":
|
||||
version "1.1.2"
|
||||
@@ -1735,6 +1734,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf"
|
||||
integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/q@^1.5.1":
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
|
||||
@@ -1750,6 +1754,22 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-dom@^18.0.0":
|
||||
version "18.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.5.tgz#330b2d472c22f796e5531446939eacef8378444a"
|
||||
integrity sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*":
|
||||
version "18.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
|
||||
integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
@@ -1762,6 +1782,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
|
||||
integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||
|
||||
"@types/serve-index@^1.9.1":
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
|
||||
@@ -3175,6 +3200,11 @@ cssstyle@^2.3.0:
|
||||
dependencies:
|
||||
cssom "~0.3.6"
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
damerau-levenshtein@^1.0.7:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
@@ -3622,10 +3652,10 @@ escodegen@^2.0.0:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-react-app@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df"
|
||||
integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g==
|
||||
eslint-config-react-app@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4"
|
||||
integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.16.0"
|
||||
"@babel/eslint-parser" "^7.16.3"
|
||||
@@ -6841,10 +6871,10 @@ react-app-polyfill@^3.0.0:
|
||||
regenerator-runtime "^0.13.9"
|
||||
whatwg-fetch "^3.6.2"
|
||||
|
||||
react-dev-utils@^12.0.0:
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526"
|
||||
integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ==
|
||||
react-dev-utils@^12.0.1:
|
||||
version "12.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"
|
||||
integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.0"
|
||||
address "^1.1.2"
|
||||
@@ -6865,25 +6895,24 @@ react-dev-utils@^12.0.0:
|
||||
open "^8.4.0"
|
||||
pkg-up "^3.1.0"
|
||||
prompts "^2.4.2"
|
||||
react-error-overlay "^6.0.10"
|
||||
react-error-overlay "^6.0.11"
|
||||
recursive-readdir "^2.2.2"
|
||||
shell-quote "^1.7.3"
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
||||
react-dom@^18.1.0:
|
||||
version "18.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
|
||||
integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
scheduler "^0.22.0"
|
||||
|
||||
react-error-overlay@^6.0.10:
|
||||
version "6.0.10"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
|
||||
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
|
||||
react-error-overlay@^6.0.11:
|
||||
version "6.0.11"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
|
||||
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
@@ -6900,10 +6929,10 @@ react-refresh@^0.11.0:
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
|
||||
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
|
||||
|
||||
react-scripts@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60"
|
||||
integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg==
|
||||
react-scripts@5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
|
||||
integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==
|
||||
dependencies:
|
||||
"@babel/core" "^7.16.0"
|
||||
"@pmmmwh/react-refresh-webpack-plugin" "^0.5.3"
|
||||
@@ -6921,7 +6950,7 @@ react-scripts@5.0.0:
|
||||
dotenv "^10.0.0"
|
||||
dotenv-expand "^5.1.0"
|
||||
eslint "^8.3.0"
|
||||
eslint-config-react-app "^7.0.0"
|
||||
eslint-config-react-app "^7.0.1"
|
||||
eslint-webpack-plugin "^3.1.1"
|
||||
file-loader "^6.2.0"
|
||||
fs-extra "^10.0.0"
|
||||
@@ -6938,7 +6967,7 @@ react-scripts@5.0.0:
|
||||
postcss-preset-env "^7.0.1"
|
||||
prompts "^2.4.2"
|
||||
react-app-polyfill "^3.0.0"
|
||||
react-dev-utils "^12.0.0"
|
||||
react-dev-utils "^12.0.1"
|
||||
react-refresh "^0.11.0"
|
||||
resolve "^1.20.0"
|
||||
resolve-url-loader "^4.0.0"
|
||||
@@ -6955,13 +6984,12 @@ react-scripts@5.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "^2.3.2"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||
react@^18.1.0:
|
||||
version "18.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
|
||||
integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
readable-stream@^2.0.1:
|
||||
version "2.3.7"
|
||||
@@ -7235,13 +7263,12 @@ saxes@^5.0.1:
|
||||
dependencies:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
scheduler@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||
scheduler@^0.22.0:
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
|
||||
integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
schema-utils@2.7.0:
|
||||
version "2.7.0"
|
||||
@@ -8156,10 +8183,10 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
||||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
web-vitals@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.3.tgz#6dca59f41dbc3fcccdb889da06191b437b18f534"
|
||||
integrity sha512-+ijpniAzcnQicXaXIN0/eHQAiV/jMt1oHGHTmz7VdAJPPkzzDhmoYPSpLgJTuFtUh+jCjxCoeTZPg7Ic+g8o7w==
|
||||
web-vitals@^2.1.4:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
|
||||
integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
|
||||
@@ -15,5 +15,5 @@ _Live Example: https://docusaurus-2-template.vercel.app_
|
||||
To get started with Docusaurus on Vercel, you can use the [Docusaurus CLI](https://v2.docusaurus.io/docs/installation#scaffold-project-website) to initialize the project:
|
||||
|
||||
```shell
|
||||
$ npx @docusaurus/init@next init my-website classic
|
||||
npx create-docusaurus@latest my-website classic
|
||||
```
|
||||
|
||||
3
examples/docusaurus-2/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
id: hola
|
||||
title: Hola
|
||||
author: Gao Wei
|
||||
author_title: Docusaurus Core Team
|
||||
author_url: https://github.com/wgao19
|
||||
author_image_url: https://avatars1.githubusercontent.com/u/2055384?v=4
|
||||
slug: first-blog-post
|
||||
title: First Blog Post
|
||||
authors:
|
||||
name: Gao Wei
|
||||
title: Docusaurus Core Team
|
||||
url: https://github.com/wgao19
|
||||
image_url: https://github.com/wgao19.png
|
||||
tags: [hola, docusaurus]
|
||||
---
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
id: hello-world
|
||||
title: Hello
|
||||
author: Endilie Yacop Sucipto
|
||||
author_title: Maintainer of Docusaurus
|
||||
author_url: https://github.com/endiliey
|
||||
author_image_url: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
Welcome to this blog. This blog is created with [**Docusaurus 2 alpha**](https://v2.docusaurus.io/).
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
This is a test post.
|
||||
|
||||
A whole bunch of other information.
|
||||
44
examples/docusaurus-2/blog/2019-05-29-long-blog-post.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
slug: long-blog-post
|
||||
title: Long Blog Post
|
||||
authors: endi
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
This is the summary of a very long blog post,
|
||||
|
||||
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
id: welcome
|
||||
title: Welcome
|
||||
author: Yangshun Tay
|
||||
author_title: Front End Engineer @ Facebook
|
||||
author_url: https://github.com/yangshun
|
||||
author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well!
|
||||
|
||||
Delete the whole directory if you don't want the blog features. As simple as that!
|
||||
20
examples/docusaurus-2/blog/2021-08-01-mdx-blog-post.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
slug: mdx-blog-post
|
||||
title: MDX Blog Post
|
||||
authors: [slorber]
|
||||
tags: [docusaurus]
|
||||
---
|
||||
|
||||
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||
|
||||
:::tip
|
||||
|
||||
Use the power of React to create interactive blog posts.
|
||||
|
||||
```js
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
```
|
||||
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
|
||||
:::
|
||||
|
After Width: | Height: | Size: 94 KiB |
25
examples/docusaurus-2/blog/2021-08-26-welcome/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
slug: welcome
|
||||
title: Welcome
|
||||
authors: [slorber, yangshun]
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||
|
||||
Simply add Markdown files (or folders) to the `blog` directory.
|
||||
|
||||
Regular blog authors can be added to `authors.yml`.
|
||||
|
||||
The blog post date can be extracted from filenames, such as:
|
||||
|
||||
- `2019-05-30-welcome.md`
|
||||
- `2019-05-30-welcome/index.md`
|
||||
|
||||
A blog post folder can be convenient to co-locate blog post images:
|
||||
|
||||

|
||||
|
||||
The blog supports tags as well!
|
||||
|
||||
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
||||
17
examples/docusaurus-2/blog/authors.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
endi:
|
||||
name: Endilie Yacop Sucipto
|
||||
title: Maintainer of Docusaurus
|
||||
url: https://github.com/endiliey
|
||||
image_url: https://github.com/endiliey.png
|
||||
|
||||
yangshun:
|
||||
name: Yangshun Tay
|
||||
title: Front End Engineer @ Facebook
|
||||
url: https://github.com/yangshun
|
||||
image_url: https://github.com/yangshun.png
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
id: doc1
|
||||
title: Style Guide
|
||||
sidebar_label: Style Guide
|
||||
---
|
||||
|
||||
You can write content using [GitHub-flavored Markdown syntax](https://github.github.com/gfm/).
|
||||
|
||||
## Markdown Syntax
|
||||
|
||||
To serve as an example page when styling markdown based Docusaurus sites.
|
||||
|
||||
## Headers
|
||||
|
||||
# H1 - Create the best documentation
|
||||
|
||||
## H2 - Create the best documentation
|
||||
|
||||
### H3 - Create the best documentation
|
||||
|
||||
#### H4 - Create the best documentation
|
||||
|
||||
##### H5 - Create the best documentation
|
||||
|
||||
###### H6 - Create the best documentation
|
||||
|
||||
---
|
||||
|
||||
## Emphasis
|
||||
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
---
|
||||
|
||||
## Lists
|
||||
|
||||
1. First ordered list item
|
||||
1. Another item ⋅⋅\* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number ⋅⋅1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ ⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅ ⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
- Unordered list can use asterisks
|
||||
|
||||
* Or minuses
|
||||
|
||||
- Or pluses
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][arbitrary case-insensitive reference text]
|
||||
|
||||
[I'm a relative reference to a repository file](../blob/master/LICENSE)
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links. http://www.example.com or <http://www.example.com> and sometimes example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
---
|
||||
|
||||
## Images
|
||||
|
||||
Here's our logo (hover to see the title text):
|
||||
|
||||
Inline-style: 
|
||||
|
||||
Reference-style: ![alt text][logo]
|
||||
|
||||
[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png 'Logo Title Text 2'
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
var s = 'JavaScript syntax highlighting';
|
||||
alert(s);
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
print(s)
|
||||
```
|
||||
|
||||
```
|
||||
No language indicated, so no syntax highlighting.
|
||||
But let's throw in a <b>tag</b>.
|
||||
```
|
||||
|
||||
```js {2}
|
||||
function highlightMe() {
|
||||
console.log('This line can be highlighted!');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- | :-----------: | -----: |
|
||||
| col 3 is | right-aligned | \$1600 |
|
||||
| col 2 is | centered | \$12 |
|
||||
| zebra stripes | are neat | \$1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
| Markdown | Less | Pretty |
|
||||
| -------- | --------- | ---------- |
|
||||
| _Still_ | `renders` | **nicely** |
|
||||
| 1 | 2 | 3 |
|
||||
|
||||
---
|
||||
|
||||
## Blockquotes
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text. This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can _put_ **Markdown** into a blockquote.
|
||||
|
||||
---
|
||||
|
||||
## Inline HTML
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
|
||||
---
|
||||
|
||||
## Line Breaks
|
||||
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a _separate paragraph_.
|
||||
|
||||
This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the _same paragraph_.
|
||||
|
||||
---
|
||||
|
||||
## Admonitions
|
||||
|
||||
:::note
|
||||
|
||||
This is a note
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
|
||||
This is a tip
|
||||
|
||||
:::
|
||||
|
||||
:::important
|
||||
|
||||
This is important
|
||||
|
||||
:::
|
||||
|
||||
:::caution
|
||||
|
||||
This is a caution
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
This is a warning
|
||||
|
||||
:::
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
id: doc2
|
||||
title: Document Number 2
|
||||
---
|
||||
|
||||
This is a link to [another document.](doc3.md) This is a link to an [external page.](http://www.example.com)
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
id: doc3
|
||||
title: This is Document Number 3
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ac euismod odio, eu consequat dui. Nullam molestie consectetur risus id imperdiet. Proin sodales ornare turpis, non mollis massa ultricies id. Nam at nibh scelerisque, feugiat ante non, dapibus tortor. Vivamus volutpat diam quis tellus elementum bibendum. Praesent semper gravida velit quis aliquam. Etiam in cursus neque. Nam lectus ligula, malesuada et mauris a, bibendum faucibus mi. Phasellus ut interdum felis. Phasellus in odio pulvinar, porttitor urna eget, fringilla lectus. Aliquam sollicitudin est eros. Mauris consectetur quam vitae mauris interdum hendrerit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
|
||||
Duis et egestas libero, imperdiet faucibus ipsum. Sed posuere eget urna vel feugiat. Vivamus a arcu sagittis, fermentum urna dapibus, congue lectus. Fusce vulputate porttitor nisl, ac cursus elit volutpat vitae. Nullam vitae ipsum egestas, convallis quam non, porta nibh. Morbi gravida erat nec neque bibendum, eu pellentesque velit posuere. Fusce aliquam erat eu massa eleifend tristique.
|
||||
|
||||
Sed consequat sollicitudin ipsum eget tempus. Integer a aliquet velit. In justo nibh, pellentesque non suscipit eget, gravida vel lacus. Donec odio ante, malesuada in massa quis, pharetra tristique ligula. Donec eros est, tristique eget finibus quis, semper non nisl. Vivamus et elit nec enim ornare placerat. Sed posuere odio a elit cursus sagittis.
|
||||
|
||||
Phasellus feugiat purus eu tortor ultrices finibus. Ut libero nibh, lobortis et libero nec, dapibus posuere eros. Sed sagittis euismod justo at consectetur. Nulla finibus libero placerat, cursus sapien at, eleifend ligula. Vivamus elit nisl, hendrerit ac nibh eu, ultrices tempus dui. Nam tellus neque, commodo non rhoncus eu, gravida in risus. Nullam id iaculis tortor.
|
||||
|
||||
Nullam at odio in sem varius tempor sit amet vel lorem. Etiam eu hendrerit nisl. Fusce nibh mauris, vulputate sit amet ex vitae, congue rhoncus nisl. Sed eget tellus purus. Nullam tempus commodo erat ut tristique. Cras accumsan massa sit amet justo consequat eleifend. Integer scelerisque vitae tellus id consectetur.
|
||||
47
examples/docusaurus-2/docs/intro.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Tutorial Intro
|
||||
|
||||
Let's discover **Docusaurus in less than 5 minutes**.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Get started by **creating a new site**.
|
||||
|
||||
Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) version 16.14 or above:
|
||||
- When installing Node.js, you are recommended to check all checkboxes related to dependencies.
|
||||
|
||||
## Generate a new site
|
||||
|
||||
Generate a new Docusaurus site using the **classic template**.
|
||||
|
||||
The classic template will automatically be added to your project after you run the command:
|
||||
|
||||
```bash
|
||||
npm init docusaurus@latest my-website classic
|
||||
```
|
||||
|
||||
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
|
||||
|
||||
The command also installs all necessary dependencies you need to run Docusaurus.
|
||||
|
||||
## Start your site
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
cd my-website
|
||||
npm run start
|
||||
```
|
||||
|
||||
The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.
|
||||
|
||||
The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.
|
||||
|
||||
Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
id: mdx
|
||||
title: Powered by MDX
|
||||
---
|
||||
|
||||
You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/).
|
||||
|
||||
export const Highlight = ({children, color}) => ( <span style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '2px',
|
||||
color: '#fff',
|
||||
padding: '0.2rem',
|
||||
}}> {children} </span> );
|
||||
|
||||
<Highlight color="#25c2a0">Docusaurus green</Highlight> and <Highlight color="#1877F2">Facebook blue</Highlight> are my favorite colors.
|
||||
|
||||
I can write **Markdown** alongside my _JSX_!
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Tutorial - Basics",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "5 minutes to learn the most important Docusaurus concepts."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Congratulations!
|
||||
|
||||
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
|
||||
|
||||
Docusaurus has **much more to offer**!
|
||||
|
||||
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
|
||||
|
||||
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
|
||||
|
||||
## What's next?
|
||||
|
||||
- Read the [official documentation](https://docusaurus.io/).
|
||||
- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout)
|
||||
- Add a [search bar](https://docusaurus.io/docs/search)
|
||||
- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase)
|
||||
- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Create a Blog Post
|
||||
|
||||
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
|
||||
|
||||
## Create your first Post
|
||||
|
||||
Create a file at `blog/2021-02-28-greetings.md`:
|
||||
|
||||
```md title="blog/2021-02-28-greetings.md"
|
||||
---
|
||||
slug: greetings
|
||||
title: Greetings!
|
||||
authors:
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
- name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
tags: [greetings]
|
||||
---
|
||||
|
||||
Congratulations, you have made your first post!
|
||||
|
||||
Feel free to play around and edit this post as much you like.
|
||||
```
|
||||
|
||||
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Create a Document
|
||||
|
||||
Documents are **groups of pages** connected through:
|
||||
|
||||
- a **sidebar**
|
||||
- **previous/next navigation**
|
||||
- **versioning**
|
||||
|
||||
## Create your first Doc
|
||||
|
||||
Create a Markdown file at `docs/hello.md`:
|
||||
|
||||
```md title="docs/hello.md"
|
||||
# Hello
|
||||
|
||||
This is my **first Docusaurus document**!
|
||||
```
|
||||
|
||||
A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello).
|
||||
|
||||
## Configure the Sidebar
|
||||
|
||||
Docusaurus automatically **creates a sidebar** from the `docs` folder.
|
||||
|
||||
Add metadata to customize the sidebar label and position:
|
||||
|
||||
```md title="docs/hello.md" {1-4}
|
||||
---
|
||||
sidebar_label: 'Hi!'
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
This is my **first Docusaurus document**!
|
||||
```
|
||||
|
||||
It is also possible to create your sidebar explicitly in `sidebars.js`:
|
||||
|
||||
```js title="sidebars.js"
|
||||
module.exports = {
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
// highlight-next-line
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
43
examples/docusaurus-2/docs/tutorial-basics/create-a-page.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Create a Page
|
||||
|
||||
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
|
||||
|
||||
- `src/pages/index.js` → `localhost:3000/`
|
||||
- `src/pages/foo.md` → `localhost:3000/foo`
|
||||
- `src/pages/foo/bar.js` → `localhost:3000/foo/bar`
|
||||
|
||||
## Create your first React Page
|
||||
|
||||
Create a file at `src/pages/my-react-page.js`:
|
||||
|
||||
```jsx title="src/pages/my-react-page.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
export default function MyReactPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>My React page</h1>
|
||||
<p>This is a React page</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page).
|
||||
|
||||
## Create your first Markdown Page
|
||||
|
||||
Create a file at `src/pages/my-markdown-page.md`:
|
||||
|
||||
```mdx title="src/pages/my-markdown-page.md"
|
||||
# My Markdown page
|
||||
|
||||
This is a Markdown page
|
||||
```
|
||||
|
||||
A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Deploy your site
|
||||
|
||||
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
|
||||
|
||||
It builds your site as simple **static HTML, JavaScript and CSS files**.
|
||||
|
||||
## Build your site
|
||||
|
||||
Build your site **for production**:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The static files are generated in the `build` folder.
|
||||
|
||||
## Deploy your site
|
||||
|
||||
Test your production build locally:
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**).
|
||||
146
examples/docusaurus-2/docs/tutorial-basics/markdown-features.mdx
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Markdown Features
|
||||
|
||||
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
|
||||
|
||||
## Front Matter
|
||||
|
||||
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
|
||||
|
||||
```text title="my-doc.md"
|
||||
// highlight-start
|
||||
---
|
||||
id: my-doc-id
|
||||
title: My document title
|
||||
description: My document description
|
||||
slug: /my-custom-url
|
||||
---
|
||||
// highlight-end
|
||||
|
||||
## Markdown heading
|
||||
|
||||
Markdown text with [links](./hello.md)
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
Regular Markdown links are supported, using url paths or relative file paths.
|
||||
|
||||
```md
|
||||
Let's see how to [Create a page](/create-a-page).
|
||||
```
|
||||
|
||||
```md
|
||||
Let's see how to [Create a page](./create-a-page.md).
|
||||
```
|
||||
|
||||
**Result:** Let's see how to [Create a page](./create-a-page.md).
|
||||
|
||||
## Images
|
||||
|
||||
Regular Markdown images are supported.
|
||||
|
||||
You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`):
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||

|
||||
|
||||
You can reference images relative to the current file as well, as shown in [the extra guides](../tutorial-extras/manage-docs-versions.md).
|
||||
|
||||
## Code Blocks
|
||||
|
||||
Markdown code blocks are supported with Syntax highlighting.
|
||||
|
||||
```jsx title="src/components/HelloDocusaurus.js"
|
||||
function HelloDocusaurus() {
|
||||
return (
|
||||
<h1>Hello, Docusaurus!</h1>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="src/components/HelloDocusaurus.js"
|
||||
function HelloDocusaurus() {
|
||||
return <h1>Hello, Docusaurus!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Admonitions
|
||||
|
||||
Docusaurus has a special syntax to create admonitions and callouts:
|
||||
|
||||
:::tip My tip
|
||||
|
||||
Use this awesome feature option
|
||||
|
||||
:::
|
||||
|
||||
:::danger Take care
|
||||
|
||||
This action is dangerous
|
||||
|
||||
:::
|
||||
|
||||
:::tip My tip
|
||||
|
||||
Use this awesome feature option
|
||||
|
||||
:::
|
||||
|
||||
:::danger Take care
|
||||
|
||||
This action is dangerous
|
||||
|
||||
:::
|
||||
|
||||
## MDX and React Components
|
||||
|
||||
[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**:
|
||||
|
||||
```jsx
|
||||
export const Highlight = ({children, color}) => (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '20px',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
alert(`You clicked the color ${color} with label ${children}`)
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
|
||||
|
||||
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
|
||||
```
|
||||
|
||||
export const Highlight = ({children, color}) => (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '20px',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
alert(`You clicked the color ${color} with label ${children}`);
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
|
||||
|
||||
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Tutorial - Extras",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,55 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Manage Docs Versions
|
||||
|
||||
Docusaurus can manage multiple versions of your docs.
|
||||
|
||||
## Create a docs version
|
||||
|
||||
Release a version 1.0 of your project:
|
||||
|
||||
```bash
|
||||
npm run docusaurus docs:version 1.0
|
||||
```
|
||||
|
||||
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
|
||||
|
||||
Your docs now have 2 versions:
|
||||
|
||||
- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs
|
||||
- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs**
|
||||
|
||||
## Add a Version Dropdown
|
||||
|
||||
To navigate seamlessly across versions, add a version dropdown.
|
||||
|
||||
Modify the `docusaurus.config.js` file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
// highlight-start
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The docs version dropdown appears in your navbar:
|
||||
|
||||

|
||||
|
||||
## Update an existing version
|
||||
|
||||
It is possible to edit versioned docs in their respective folder:
|
||||
|
||||
- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello`
|
||||
- `docs/hello.md` updates `http://localhost:3000/docs/next/hello`
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Translate your site
|
||||
|
||||
Let's translate `docs/intro.md` to French.
|
||||
|
||||
## Configure i18n
|
||||
|
||||
Modify `docusaurus.config.js` to add support for the `fr` locale:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Translate a doc
|
||||
|
||||
Copy the `docs/intro.md` file to the `i18n/fr` folder:
|
||||
|
||||
```bash
|
||||
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
|
||||
|
||||
cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md
|
||||
```
|
||||
|
||||
Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French.
|
||||
|
||||
## Start your localized site
|
||||
|
||||
Start your site on the French locale:
|
||||
|
||||
```bash
|
||||
npm run start -- --locale fr
|
||||
```
|
||||
|
||||
Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated.
|
||||
|
||||
:::caution
|
||||
|
||||
In development, you can only use one locale at a same time.
|
||||
|
||||
:::
|
||||
|
||||
## Add a Locale Dropdown
|
||||
|
||||
To navigate seamlessly across languages, add a locale dropdown.
|
||||
|
||||
Modify the `docusaurus.config.js` file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
// highlight-start
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The locale dropdown now appears in your navbar:
|
||||
|
||||

|
||||
|
||||
## Build your localized site
|
||||
|
||||
Build your site for a specific locale:
|
||||
|
||||
```bash
|
||||
npm run build -- --locale fr
|
||||
```
|
||||
|
||||
Or build your site to include all the locales at once:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
@@ -1,26 +1,75 @@
|
||||
module.exports = {
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require('prism-react-renderer/themes/github');
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'My Site',
|
||||
tagline: 'The tagline of my site',
|
||||
tagline: 'Dinosaurs are cool',
|
||||
url: 'https://your-docusaurus-test-site.com',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'facebook', // Usually your GitHub org/user name.
|
||||
projectName: 'docusaurus', // Usually your repo name.
|
||||
themeConfig: {
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
title: 'My Site',
|
||||
logo: {
|
||||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
to: 'docs/doc1',
|
||||
activeBasePath: 'docs',
|
||||
label: 'Docs',
|
||||
type: 'doc',
|
||||
docId: 'intro',
|
||||
position: 'left',
|
||||
label: 'Tutorial',
|
||||
},
|
||||
{to: 'blog', label: 'Blog', position: 'left'},
|
||||
{to: '/blog', label: 'Blog', position: 'left'},
|
||||
{
|
||||
href: 'https://github.com/facebook/docusaurus',
|
||||
label: 'GitHub',
|
||||
@@ -35,12 +84,8 @@ module.exports = {
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{
|
||||
label: 'Style Guide',
|
||||
to: 'docs/doc1',
|
||||
},
|
||||
{
|
||||
label: 'Second Doc',
|
||||
to: 'docs/doc2',
|
||||
label: 'Tutorial',
|
||||
to: '/docs/intro',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -66,7 +111,7 @@ module.exports = {
|
||||
items: [
|
||||
{
|
||||
label: 'Blog',
|
||||
to: 'blog',
|
||||
to: '/blog',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
@@ -77,27 +122,11 @@ module.exports = {
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
presets: [
|
||||
[
|
||||
'@docusaurus/preset-classic',
|
||||
{
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
// Please change this to your repo.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website/',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website/blog/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
{
|
||||
"name": "docusaurus-2",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy"
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-alpha.54",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.54",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4"
|
||||
"@docusaurus/core": "2.0.1",
|
||||
"@docusaurus/preset-classic": "2.0.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
@@ -26,5 +34,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
module.exports = {
|
||||
someSidebar: {
|
||||
Docusaurus: ['doc1', 'doc2', 'doc3'],
|
||||
Features: ['mdx'],
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const FeatureList = [
|
||||
{
|
||||
title: 'Easy to Use',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Focus on What Matters',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Powered by React',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({Svg, title, description}) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* stylelint-disable docusaurus/copyright-header */
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
@@ -7,19 +6,25 @@
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: rgb(33, 175, 144);
|
||||
--ifm-color-primary-darker: rgb(31, 165, 136);
|
||||
--ifm-color-primary-darkest: rgb(26, 136, 112);
|
||||
--ifm-color-primary-light: rgb(70, 203, 174);
|
||||
--ifm-color-primary-lighter: rgb(102, 212, 189);
|
||||
--ifm-color-primary-lightest: rgb(146, 224, 208);
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgb(72, 77, 91);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@@ -1,97 +1,41 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import styles from './styles.module.css';
|
||||
import Layout from '@theme/Layout';
|
||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: <>Easy to Use</>,
|
||||
imageUrl: 'img/undraw_docusaurus_mountain.svg',
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <>Focus on What Matters</>,
|
||||
imageUrl: 'img/undraw_docusaurus_tree.svg',
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <>Powered by React</>,
|
||||
imageUrl: 'img/undraw_docusaurus_react.svg',
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
import styles from './index.module.css';
|
||||
|
||||
function Feature({imageUrl, title, description}) {
|
||||
const imgUrl = useBaseUrl(imageUrl);
|
||||
function HomepageHeader() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<div className={classnames('col col--4', styles.feature)}>
|
||||
{imgUrl && (
|
||||
<div className="text--center">
|
||||
<img className={styles.featureImage} src={imgUrl} alt={title} />
|
||||
</div>
|
||||
)}
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const context = useDocusaurusContext();
|
||||
const {siteConfig = {}} = context;
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />">
|
||||
<header className={classnames('hero hero--primary', styles.heroBanner)}>
|
||||
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className={classnames(
|
||||
'button button--outline button--secondary button--lg',
|
||||
styles.getStarted,
|
||||
)}
|
||||
to={useBaseUrl('docs/doc1')}>
|
||||
Get Started
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro">
|
||||
Docusaurus Tutorial - 5min ⏱️
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />">
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
{features && features.length && (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{features.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* stylelint-disable docusaurus/copyright-header */
|
||||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
@@ -11,7 +10,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 966px) {
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
}
|
||||
@@ -22,15 +21,3 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureImage {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
7
examples/docusaurus-2/src/pages/markdown-page.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
||||
BIN
examples/docusaurus-2/static/img/docusaurus.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 3.5 KiB |
@@ -1,4 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
|
||||
<title>Easy to Use</title>
|
||||
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
|
||||
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
|
||||
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -1,4 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
|
||||
<title>Powered by React</title>
|
||||
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
|
||||
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
|
||||
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
18
examples/hydrogen/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Shopify Hydrogen",
|
||||
"image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:0-16",
|
||||
"settings": {},
|
||||
"extensions": [
|
||||
"graphql.vscode-graphql",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"forwardPorts": [3000],
|
||||
"postCreateCommand": "yarn install",
|
||||
"postStartCommand": "yarn dev",
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"git": "latest"
|
||||
}
|
||||
}
|
||||
8
examples/hydrogen/.eslintrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:hydrogen/recommended', 'plugin:hydrogen/typescript'],
|
||||
rules: {
|
||||
'node/no-missing-import': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
};
|
||||
79
examples/hydrogen/.gitignore
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Vite output
|
||||
dist
|
||||
|
||||
.vercel
|
||||
50
examples/hydrogen/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Hydrogen
|
||||
|
||||
[Hydrogen](https://shopify.dev/custom-storefronts/hydrogen) is a React framework and SDK that you can use to build fast and dynamic Shopify custom storefronts.
|
||||
|
||||
## Deploy Your Own
|
||||
|
||||
Deploy your own Hydrogen project with Vercel.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/vercel/vercel/tree/main/examples/hydrogen&template=hydrogen)
|
||||
|
||||
_Live Example: https://hydrogen-template.vercel.app_
|
||||
|
||||
## Getting started
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Node.js version 16.5.0 or higher
|
||||
- Yarn
|
||||
|
||||
To create a new Hydrogen app, run:
|
||||
|
||||
```bash
|
||||
npm init @shopify/hydrogen
|
||||
```
|
||||
|
||||
## Running the dev server
|
||||
|
||||
Then `cd` into the new directory and run:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Remember to update `hydrogen.config.js` with your shop's domain and Storefront API token!
|
||||
|
||||
## Building for production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Previewing a production build
|
||||
|
||||
To run a local preview of your Hydrogen app in an environment similar to Oxygen, build your Hydrogen app and then run `npm run preview`:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
18
examples/hydrogen/hydrogen.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {defineConfig, CookieSessionStorage} from '@shopify/hydrogen/config';
|
||||
|
||||
export default defineConfig({
|
||||
shopify: {
|
||||
defaultCountryCode: 'US',
|
||||
defaultLanguageCode: 'EN',
|
||||
storeDomain: 'hydrogen-preview.myshopify.com',
|
||||
storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
|
||||
storefrontApiVersion: '2022-07',
|
||||
},
|
||||
session: CookieSessionStorage('__session', {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: 'Strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
}),
|
||||
});
|
||||
17
examples/hydrogen/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hydrogen</title>
|
||||
<link rel="stylesheet" href="/src/styles/index.css" />
|
||||
<link rel="preconnect" href="https://cdn.shopify.com" />
|
||||
<link rel="preconnect" href="https://shop.app/" />
|
||||
<link rel="preconnect" href="https://hydrogen-preview.myshopify.com/" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/@shopify/hydrogen/entry-client"></script>
|
||||
</body>
|
||||
</html>
|
||||
49
examples/hydrogen/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "hydrogen",
|
||||
"description": "Demo store template for @shopify/hydrogen",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "shopify hydrogen dev",
|
||||
"build": "shopify hydrogen build",
|
||||
"preview": "shopify hydrogen preview",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
|
||||
"lint-ts": "tsc --noEmit",
|
||||
"test": "WATCH=true vitest",
|
||||
"test:ci": "yarn build -t node && vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shopify/cli": "3.0.27",
|
||||
"@shopify/cli-hydrogen": "3.0.27",
|
||||
"@shopify/prettier-config": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@types/react": "^18.0.14",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-plugin-hydrogen": "^0.12.2",
|
||||
"playwright": "^1.22.2",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-preset-env": "^7.6.0",
|
||||
"prettier": "^2.3.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"typescript": "^4.7.2",
|
||||
"vite": "^2.9.0",
|
||||
"vitest": "^0.15.2"
|
||||
},
|
||||
"prettier": "@shopify/prettier-config",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.4",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@shopify/hydrogen": "^1.0.2",
|
||||
"clsx": "^1.1.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-use": "^17.4.0",
|
||||
"title": "^3.4.4",
|
||||
"typographic-base": "^1.0.4"
|
||||
},
|
||||
"author": "nrajlich"
|
||||
}
|
||||
10
examples/hydrogen/postcss.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
'postcss-preset-env': {
|
||||
features: {'nesting-rules': false},
|
||||
},
|
||||
},
|
||||
};
|
||||
BIN
examples/hydrogen/public/fonts/IBMPlexSerif-Text.woff2
Normal file
BIN
examples/hydrogen/public/fonts/IBMPlexSerif-TextItalic.woff2
Normal file
48
examples/hydrogen/src/App.server.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import {Suspense} from 'react';
|
||||
import renderHydrogen from '@shopify/hydrogen/entry-server';
|
||||
import {
|
||||
FileRoutes,
|
||||
type HydrogenRouteProps,
|
||||
PerformanceMetrics,
|
||||
PerformanceMetricsDebug,
|
||||
Route,
|
||||
Router,
|
||||
ShopifyAnalytics,
|
||||
ShopifyProvider,
|
||||
CartProvider,
|
||||
} from '@shopify/hydrogen';
|
||||
|
||||
import {HeaderFallback} from '~/components';
|
||||
import type {CountryCode} from '@shopify/hydrogen/storefront-api-types';
|
||||
import {DefaultSeo, NotFound} from '~/components/index.server';
|
||||
|
||||
function App({request}: HydrogenRouteProps) {
|
||||
const pathname = new URL(request.normalizedUrl).pathname;
|
||||
const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname);
|
||||
const countryCode = localeMatch ? (localeMatch[1] as CountryCode) : undefined;
|
||||
|
||||
const isHome = pathname === `/${countryCode ? countryCode + '/' : ''}`;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<HeaderFallback isHome={isHome} />}>
|
||||
<ShopifyProvider countryCode={countryCode}>
|
||||
<CartProvider countryCode={countryCode}>
|
||||
<Suspense>
|
||||
<DefaultSeo />
|
||||
</Suspense>
|
||||
<Router>
|
||||
<FileRoutes
|
||||
basePath={countryCode ? `/${countryCode}/` : undefined}
|
||||
/>
|
||||
<Route path="*" page={<NotFound />} />
|
||||
</Router>
|
||||
</CartProvider>
|
||||
<PerformanceMetrics />
|
||||
{import.meta.env.DEV && <PerformanceMetricsDebug />}
|
||||
<ShopifyAnalytics />
|
||||
</ShopifyProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default renderHydrogen(App);
|
||||
28
examples/hydrogen/src/assets/favicon.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
||||
<style>
|
||||
.stroke {
|
||||
stroke: #000;
|
||||
}
|
||||
.fill {
|
||||
fill: #000;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.stroke {
|
||||
stroke: #fff;
|
||||
}
|
||||
.fill {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="stroke"
|
||||
fill-rule="evenodd"
|
||||
d="M16.1 16.04 1 8.02 6.16 5.3l5.82 3.09 4.88-2.57-5.82-3.1L16.21 0l15.1 8.02-5.17 2.72-5.5-2.91-4.88 2.57 5.5 2.92-5.16 2.72Z"
|
||||
/>
|
||||
<path
|
||||
class="fill"
|
||||
fill-rule="evenodd"
|
||||
d="M16.1 32 1 23.98l5.16-2.72 5.82 3.08 4.88-2.57-5.82-3.08 5.17-2.73 15.1 8.02-5.17 2.72-5.5-2.92-4.88 2.58 5.5 2.92L16.1 32Z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
135
examples/hydrogen/src/components/CountrySelector.client.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import {useCallback, useState, Suspense} from 'react';
|
||||
import {useLocalization, fetchSync} from '@shopify/hydrogen';
|
||||
// @ts-expect-error @headlessui/react incompatibility with node16 resolution
|
||||
import {Listbox} from '@headlessui/react';
|
||||
|
||||
import {IconCheck, IconCaret} from '~/components';
|
||||
import {useMemo} from 'react';
|
||||
import type {
|
||||
Country,
|
||||
CountryCode,
|
||||
} from '@shopify/hydrogen/storefront-api-types';
|
||||
|
||||
/**
|
||||
* A client component that selects the appropriate country to display for products on a website
|
||||
*/
|
||||
export function CountrySelector() {
|
||||
const [listboxOpen, setListboxOpen] = useState(false);
|
||||
const {
|
||||
country: {isoCode},
|
||||
} = useLocalization();
|
||||
const currentCountry = useMemo<{name: string; isoCode: CountryCode}>(() => {
|
||||
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
|
||||
type: 'region',
|
||||
});
|
||||
|
||||
return {
|
||||
name: regionNamesInEnglish.of(isoCode)!,
|
||||
isoCode: isoCode as CountryCode,
|
||||
};
|
||||
}, [isoCode]);
|
||||
|
||||
const setCountry = useCallback<(country: Country) => void>(
|
||||
({isoCode: newIsoCode}) => {
|
||||
const currentPath = window.location.pathname;
|
||||
let redirectPath;
|
||||
|
||||
if (newIsoCode !== 'US') {
|
||||
if (currentCountry.isoCode === 'US') {
|
||||
redirectPath = `/${newIsoCode.toLowerCase()}${currentPath}`;
|
||||
} else {
|
||||
redirectPath = `/${newIsoCode.toLowerCase()}${currentPath.substring(
|
||||
currentPath.indexOf('/', 1),
|
||||
)}`;
|
||||
}
|
||||
} else {
|
||||
redirectPath = `${currentPath.substring(currentPath.indexOf('/', 1))}`;
|
||||
}
|
||||
|
||||
window.location.href = redirectPath;
|
||||
},
|
||||
[currentCountry],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Listbox onChange={setCountry}>
|
||||
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
|
||||
{({open}) => {
|
||||
setTimeout(() => setListboxOpen(open));
|
||||
return (
|
||||
<>
|
||||
<Listbox.Button
|
||||
className={`flex items-center justify-between w-full py-3 px-4 border ${
|
||||
open ? 'rounded-b md:rounded-t md:rounded-b-none' : 'rounded'
|
||||
} border-contrast/30 dark:border-white`}
|
||||
>
|
||||
<span className="">{currentCountry.name}</span>
|
||||
<IconCaret direction={open ? 'up' : 'down'} />
|
||||
</Listbox.Button>
|
||||
|
||||
<Listbox.Options
|
||||
className={`border-t-contrast/30 border-contrast/30 bg-primary dark:bg-contrast absolute bottom-12 z-10 grid
|
||||
h-48 w-full overflow-y-scroll rounded-t border dark:border-white px-2 py-2
|
||||
transition-[max-height] duration-150 sm:bottom-auto md:rounded-b md:rounded-t-none
|
||||
md:border-t-0 md:border-b ${
|
||||
listboxOpen ? 'max-h-48' : 'max-h-0'
|
||||
}`}
|
||||
>
|
||||
{listboxOpen && (
|
||||
<Suspense fallback={<div className="p-2">Loading…</div>}>
|
||||
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
|
||||
<Countries
|
||||
selectedCountry={currentCountry}
|
||||
getClassName={(active) => {
|
||||
return `text-contrast dark:text-primary bg-primary
|
||||
dark:bg-contrast w-full p-2 transition rounded
|
||||
flex justify-start items-center text-left cursor-pointer ${
|
||||
active ? 'bg-primary/10' : null
|
||||
}`;
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</Listbox.Options>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Countries({
|
||||
selectedCountry,
|
||||
getClassName,
|
||||
}: {
|
||||
selectedCountry: Pick<Country, 'isoCode' | 'name'>;
|
||||
getClassName: (active: boolean) => string;
|
||||
}) {
|
||||
const countries: Country[] = fetchSync('/api/countries').json();
|
||||
|
||||
return (countries || []).map((country) => {
|
||||
const isSelected = country.isoCode === selectedCountry.isoCode;
|
||||
|
||||
return (
|
||||
<Listbox.Option key={country.isoCode} value={country}>
|
||||
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
|
||||
{({active}) => (
|
||||
<div
|
||||
className={`text-contrast dark:text-primary ${getClassName(
|
||||
active,
|
||||
)}`}
|
||||
>
|
||||
{country.name}
|
||||
{isSelected ? (
|
||||
<span className="ml-2">
|
||||
<IconCheck />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
});
|
||||
}
|
||||
22
examples/hydrogen/src/components/CustomFont.client.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
// When making building your custom storefront, you will most likely want to
|
||||
// use custom fonts as well. These are often implemented without critical
|
||||
// performance optimizations.
|
||||
|
||||
// Below, you'll find the markup needed to optimally render a pair of web fonts
|
||||
// that we will use on our journal articles. This typeface, IBM Plex,
|
||||
// can be found at: https://www.ibm.com/plex/, as well as on
|
||||
// Google Fonts: https://fonts.google.com/specimen/IBM+Plex+Serif. We included
|
||||
// these locally since you’ll most likely be using commercially licensed fonts.
|
||||
|
||||
// When implementing a custom font, specifying the Unicode range you need,
|
||||
// and using `font-display: swap` will help you improve your performance.
|
||||
|
||||
// For fonts that appear in the critical rendering path, you can speed up
|
||||
// performance even more by including a <link> tag in your HTML.
|
||||
|
||||
// In a production environment, you will likely want to include the below
|
||||
// markup right in your index.html and index.css files.
|
||||
|
||||
import '../styles/custom-font.css';
|
||||
|
||||
export function CustomFont() {}
|
||||
37
examples/hydrogen/src/components/DefaultSeo.server.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import {CacheLong, gql, Seo, useShopQuery} from '@shopify/hydrogen';
|
||||
|
||||
/**
|
||||
* A server component that fetches a `shop.name` and sets default values and templates for every page on a website
|
||||
*/
|
||||
export function DefaultSeo() {
|
||||
const {
|
||||
data: {
|
||||
shop: {name, description},
|
||||
},
|
||||
} = useShopQuery({
|
||||
query: SHOP_QUERY,
|
||||
cache: CacheLong(),
|
||||
preload: '*',
|
||||
});
|
||||
|
||||
return (
|
||||
// @ts-ignore TODO: Fix types
|
||||
<Seo
|
||||
type="defaultSeo"
|
||||
data={{
|
||||
title: name,
|
||||
description,
|
||||
titleTemplate: `%s · ${name}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SHOP_QUERY = gql`
|
||||
query shopInfo {
|
||||
shop {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
||||
30
examples/hydrogen/src/components/HeaderFallback.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
export function HeaderFallback({isHome}: {isHome?: boolean}) {
|
||||
const styles = isHome
|
||||
? 'bg-primary/80 dark:bg-contrast/60 text-contrast dark:text-primary shadow-darkHeader'
|
||||
: 'bg-contrast/80 text-primary';
|
||||
return (
|
||||
<header
|
||||
role="banner"
|
||||
className={`${styles} flex h-nav items-center backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-8 px-12 py-8`}
|
||||
>
|
||||
<div className="flex space-x-4">
|
||||
<Box isHome={isHome} />
|
||||
<Box isHome={isHome} />
|
||||
<Box isHome={isHome} />
|
||||
<Box isHome={isHome} />
|
||||
<Box isHome={isHome} />
|
||||
</div>
|
||||
<Box isHome={isHome} wide={true} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function Box({wide, isHome}: {wide?: boolean; isHome?: boolean}) {
|
||||
return (
|
||||
<div
|
||||
className={`h-6 rounded-sm ${wide ? 'w-32' : 'w-16'} ${
|
||||
isHome ? 'bg-primary/60' : 'bg-primary/20'
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import {useState} from 'react';
|
||||
import {useNavigate} from '@shopify/hydrogen/client';
|
||||
|
||||
export function AccountActivateForm({
|
||||
id,
|
||||
activationToken,
|
||||
}: {
|
||||
id: string;
|
||||
activationToken: string;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [submitError, setSubmitError] = useState<null | string>(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState<null | string>(null);
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('');
|
||||
const [passwordConfirmError, setPasswordConfirmError] = useState<
|
||||
null | string
|
||||
>(null);
|
||||
|
||||
function passwordValidation(
|
||||
form: HTMLFormElement & {password: HTMLInputElement},
|
||||
) {
|
||||
setPasswordError(null);
|
||||
setPasswordConfirmError(null);
|
||||
|
||||
let hasError = false;
|
||||
|
||||
if (!form.password.validity.valid) {
|
||||
hasError = true;
|
||||
setPasswordError(
|
||||
form.password.validity.valueMissing
|
||||
? 'Please enter a password'
|
||||
: 'Passwords must be at least 6 characters',
|
||||
);
|
||||
}
|
||||
|
||||
if (!form.passwordConfirm.validity.valid) {
|
||||
hasError = true;
|
||||
setPasswordConfirmError(
|
||||
form.password.validity.valueMissing
|
||||
? 'Please re-enter a password'
|
||||
: 'Passwords must be at least 6 characters',
|
||||
);
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
hasError = true;
|
||||
setPasswordConfirmError('The two passwords entered did not match.');
|
||||
}
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
async function onSubmit(
|
||||
event: React.FormEvent<HTMLFormElement & {password: HTMLInputElement}>,
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
if (passwordValidation(event.currentTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await callActivateApi({
|
||||
id,
|
||||
activationToken,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
setSubmitError(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/account');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<h1 className="text-4xl">Activate Account.</h1>
|
||||
<p className="mt-4">Create your password to activate your account.</p>
|
||||
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
|
||||
{submitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-primary/30">
|
||||
<p className="m-4 text-s text-contrast">{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<input
|
||||
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary placeholder:text-primary/30 leading-tight focus:shadow-outline ${
|
||||
passwordError ? ' border-notice' : 'border-primary'
|
||||
}`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
value={password}
|
||||
minLength={8}
|
||||
required
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={`text-red-500 text-xs ${
|
||||
!passwordError ? 'invisible' : ''
|
||||
}`}
|
||||
>
|
||||
{passwordError}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
passwordConfirmError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Re-enter password"
|
||||
aria-label="Re-enter password"
|
||||
value={passwordConfirm}
|
||||
required
|
||||
minLength={8}
|
||||
onChange={(event) => {
|
||||
setPasswordConfirm(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={`text-red-500 text-xs ${
|
||||
!passwordConfirmError ? 'invisible' : ''
|
||||
}`}
|
||||
>
|
||||
{passwordConfirmError}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="block w-full px-4 py-2 text-contrast uppercase bg-gray-900 focus:shadow-outline"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function callActivateApi({
|
||||
id,
|
||||
activationToken,
|
||||
password,
|
||||
}: {
|
||||
id: string;
|
||||
activationToken: string;
|
||||
password: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/account/activate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({id, activationToken, password}),
|
||||
});
|
||||
if (res.ok) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
error: error.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import {useState, useMemo, MouseEventHandler} from 'react';
|
||||
|
||||
import {Text, Button} from '~/components/elements';
|
||||
import {Modal} from '../index';
|
||||
import {AccountAddressEdit, AccountDeleteAddress} from '../index';
|
||||
|
||||
export function AccountAddressBook({
|
||||
addresses,
|
||||
defaultAddress,
|
||||
}: {
|
||||
addresses: any[];
|
||||
defaultAddress: any;
|
||||
}) {
|
||||
const [editingAddress, setEditingAddress] = useState(null);
|
||||
const [deletingAddress, setDeletingAddress] = useState(null);
|
||||
|
||||
const {fullDefaultAddress, addressesWithoutDefault} = useMemo(() => {
|
||||
const defaultAddressIndex = addresses.findIndex(
|
||||
(address) => address.id === defaultAddress,
|
||||
);
|
||||
return {
|
||||
addressesWithoutDefault: [
|
||||
...addresses.slice(0, defaultAddressIndex),
|
||||
...addresses.slice(defaultAddressIndex + 1, addresses.length),
|
||||
],
|
||||
fullDefaultAddress: addresses[defaultAddressIndex],
|
||||
};
|
||||
}, [addresses, defaultAddress]);
|
||||
|
||||
function close() {
|
||||
setEditingAddress(null);
|
||||
setDeletingAddress(null);
|
||||
}
|
||||
|
||||
function editAddress(address: any) {
|
||||
setEditingAddress(address);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{deletingAddress ? (
|
||||
<Modal close={close}>
|
||||
<AccountDeleteAddress addressId={deletingAddress} close={close} />
|
||||
</Modal>
|
||||
) : null}
|
||||
{editingAddress ? (
|
||||
<Modal close={close}>
|
||||
<AccountAddressEdit
|
||||
address={editingAddress}
|
||||
defaultAddress={fullDefaultAddress === editingAddress}
|
||||
close={close}
|
||||
/>
|
||||
</Modal>
|
||||
) : null}
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h3 className="font-bold text-lead">Address Book</h3>
|
||||
<div>
|
||||
{!addresses?.length ? (
|
||||
<Text className="mb-1" width="narrow" as="p" size="copy">
|
||||
You haven't saved any addresses yet.
|
||||
</Text>
|
||||
) : null}
|
||||
<div className="w-48">
|
||||
<Button
|
||||
className="mt-2 text-sm w-full mb-6"
|
||||
onClick={() => {
|
||||
editAddress({
|
||||
/** empty address */
|
||||
});
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
Add an Address
|
||||
</Button>
|
||||
</div>
|
||||
{addresses?.length ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{fullDefaultAddress ? (
|
||||
<Address
|
||||
address={fullDefaultAddress}
|
||||
defaultAddress
|
||||
setDeletingAddress={setDeletingAddress.bind(
|
||||
null,
|
||||
fullDefaultAddress.originalId,
|
||||
)}
|
||||
editAddress={editAddress}
|
||||
/>
|
||||
) : null}
|
||||
{addressesWithoutDefault.map((address) => (
|
||||
<Address
|
||||
key={address.id}
|
||||
address={address}
|
||||
setDeletingAddress={setDeletingAddress.bind(
|
||||
null,
|
||||
address.originalId,
|
||||
)}
|
||||
editAddress={editAddress}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Address({
|
||||
address,
|
||||
defaultAddress,
|
||||
editAddress,
|
||||
setDeletingAddress,
|
||||
}: {
|
||||
address: any;
|
||||
defaultAddress?: boolean;
|
||||
editAddress: (address: any) => void;
|
||||
setDeletingAddress: MouseEventHandler<HTMLButtonElement>;
|
||||
}) {
|
||||
return (
|
||||
<div className="lg:p-8 p-6 border border-gray-200 rounded flex flex-col">
|
||||
{defaultAddress ? (
|
||||
<div className="mb-3 flex flex-row">
|
||||
<span className="px-3 py-1 text-xs font-medium rounded-full bg-primary/20 text-primary/50">
|
||||
Default
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<ul className="flex-1 flex-row">
|
||||
{address.firstName || address.lastName ? (
|
||||
<li>
|
||||
{(address.firstName && address.firstName + ' ') + address.lastName}
|
||||
</li>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{address.formatted ? (
|
||||
address.formatted.map((line: string) => <li key={line}>{line}</li>)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-row font-medium mt-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
editAddress(address);
|
||||
}}
|
||||
className="text-left underline text-sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={setDeletingAddress}
|
||||
className="text-left text-primary/50 ml-6 text-sm"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import {useMemo, useState} from 'react';
|
||||
import {useRenderServerComponents} from '~/lib/utils';
|
||||
|
||||
import {Button, Text} from '~/components';
|
||||
|
||||
export function AccountAddressEdit({
|
||||
address,
|
||||
defaultAddress,
|
||||
close,
|
||||
}: {
|
||||
address: any;
|
||||
defaultAddress: boolean;
|
||||
close: () => void;
|
||||
}) {
|
||||
const isNewAddress = useMemo(() => !Object.keys(address).length, [address]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<null | string>(null);
|
||||
const [address1, setAddress1] = useState(address?.address1 || '');
|
||||
const [address2, setAddress2] = useState(address?.address2 || '');
|
||||
const [firstName, setFirstName] = useState(address?.firstName || '');
|
||||
const [lastName, setLastName] = useState(address?.lastName || '');
|
||||
const [company, setCompany] = useState(address?.company || '');
|
||||
const [country, setCountry] = useState(address?.country || '');
|
||||
const [province, setProvince] = useState(address?.province || '');
|
||||
const [city, setCity] = useState(address?.city || '');
|
||||
const [zip, setZip] = useState(address?.zip || '');
|
||||
const [phone, setPhone] = useState(address?.phone || '');
|
||||
const [isDefaultAddress, setIsDefaultAddress] = useState(defaultAddress);
|
||||
|
||||
// Necessary for edits to show up on the main page
|
||||
const renderServerComponents = useRenderServerComponents();
|
||||
|
||||
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
setSaving(true);
|
||||
|
||||
const response = await callUpdateAddressApi({
|
||||
id: address?.originalId,
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
country,
|
||||
province,
|
||||
city,
|
||||
zip,
|
||||
phone,
|
||||
isDefaultAddress,
|
||||
});
|
||||
|
||||
setSaving(false);
|
||||
|
||||
if (response.error) {
|
||||
setSubmitError(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
renderServerComponents();
|
||||
close();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text className="mt-4 mb-6" as="h3" size="lead">
|
||||
{isNewAddress ? 'Add address' : 'Edit address'}
|
||||
</Text>
|
||||
<div className="max-w-lg">
|
||||
<form noValidate onSubmit={onSubmit}>
|
||||
{submitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-red-100 rounded">
|
||||
<p className="m-4 text-sm text-red-900">{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="firstname"
|
||||
name="firstname"
|
||||
required
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
placeholder="First name"
|
||||
aria-label="First name"
|
||||
value={firstName}
|
||||
onChange={(event) => {
|
||||
setFirstName(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="lastname"
|
||||
name="lastname"
|
||||
required
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
placeholder="Last name"
|
||||
aria-label="Last name"
|
||||
value={lastName}
|
||||
onChange={(event) => {
|
||||
setLastName(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="company"
|
||||
name="company"
|
||||
type="text"
|
||||
autoComplete="organization"
|
||||
placeholder="Company"
|
||||
aria-label="Company"
|
||||
value={company}
|
||||
onChange={(event) => {
|
||||
setCompany(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="street1"
|
||||
name="street1"
|
||||
type="text"
|
||||
autoComplete="address-line1"
|
||||
placeholder="Address line 1*"
|
||||
required
|
||||
aria-label="Address line 1"
|
||||
value={address1}
|
||||
onChange={(event) => {
|
||||
setAddress1(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="address2"
|
||||
name="address2"
|
||||
type="text"
|
||||
autoComplete="address-line2"
|
||||
placeholder="Addresss line 2"
|
||||
aria-label="Address line 2"
|
||||
value={address2}
|
||||
onChange={(event) => {
|
||||
setAddress2(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="city"
|
||||
name="city"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="address-level2"
|
||||
placeholder="City"
|
||||
aria-label="City"
|
||||
value={city}
|
||||
onChange={(event) => {
|
||||
setCity(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="state"
|
||||
name="state"
|
||||
type="text"
|
||||
autoComplete="address-level1"
|
||||
placeholder="State / Province"
|
||||
required
|
||||
aria-label="State"
|
||||
value={province}
|
||||
onChange={(event) => {
|
||||
setProvince(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="zip"
|
||||
name="zip"
|
||||
type="text"
|
||||
autoComplete="postal-code"
|
||||
placeholder="Zip / Postal Code"
|
||||
required
|
||||
aria-label="Zip"
|
||||
value={zip}
|
||||
onChange={(event) => {
|
||||
setZip(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="country"
|
||||
name="country"
|
||||
type="text"
|
||||
autoComplete="country-name"
|
||||
placeholder="Country"
|
||||
required
|
||||
aria-label="Country"
|
||||
value={country}
|
||||
onChange={(event) => {
|
||||
setCountry(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
autoComplete="tel"
|
||||
placeholder="Phone"
|
||||
aria-label="Phone"
|
||||
value={phone}
|
||||
onChange={(event) => {
|
||||
setPhone(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
name="defaultAddress"
|
||||
id="defaultAddress"
|
||||
checked={isDefaultAddress}
|
||||
className="border-gray-500 rounded-sm cursor-pointer border-1"
|
||||
onChange={() => setIsDefaultAddress(!isDefaultAddress)}
|
||||
/>
|
||||
<label
|
||||
className="inline-block ml-2 text-sm cursor-pointer"
|
||||
htmlFor="defaultAddress"
|
||||
>
|
||||
Set as default address
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<Button
|
||||
className="w-full rounded focus:shadow-outline"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={saving}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="w-full mt-2 rounded focus:shadow-outline"
|
||||
variant="secondary"
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callUpdateAddressApi({
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
country,
|
||||
province,
|
||||
city,
|
||||
phone,
|
||||
zip,
|
||||
isDefaultAddress,
|
||||
}: {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
company: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
country: string;
|
||||
province: string;
|
||||
city: string;
|
||||
phone: string;
|
||||
zip: string;
|
||||
isDefaultAddress: boolean;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
id ? `/account/address/${encodeURIComponent(id)}` : '/account/address',
|
||||
{
|
||||
method: id ? 'PATCH' : 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
country,
|
||||
province,
|
||||
city,
|
||||
phone,
|
||||
zip,
|
||||
isDefaultAddress,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (res.ok) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (_e) {
|
||||
return {
|
||||
error: 'Error saving address. Please try again.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import {useState} from 'react';
|
||||
import {useNavigate, Link} from '@shopify/hydrogen/client';
|
||||
|
||||
import {emailValidation, passwordValidation} from '~/lib/utils';
|
||||
|
||||
import {callLoginApi} from './AccountLoginForm.client';
|
||||
|
||||
interface FormElements {
|
||||
email: HTMLInputElement;
|
||||
password: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function AccountCreateForm() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [submitError, setSubmitError] = useState<null | string>(null);
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailError, setEmailError] = useState<null | string>(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState<null | string>(null);
|
||||
|
||||
async function onSubmit(
|
||||
event: React.FormEvent<HTMLFormElement & FormElements>,
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
setEmailError(null);
|
||||
setPasswordError(null);
|
||||
setSubmitError(null);
|
||||
|
||||
const newEmailError = emailValidation(event.currentTarget.email);
|
||||
if (newEmailError) {
|
||||
setEmailError(newEmailError);
|
||||
}
|
||||
|
||||
const newPasswordError = passwordValidation(event.currentTarget.password);
|
||||
if (newPasswordError) {
|
||||
setPasswordError(newPasswordError);
|
||||
}
|
||||
|
||||
if (newEmailError || newPasswordError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountCreateResponse = await callAccountCreateApi({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (accountCreateResponse.error) {
|
||||
setSubmitError(accountCreateResponse.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// this can be avoided if customerCreate mutation returns customerAccessToken
|
||||
await callLoginApi({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
navigate('/account');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center my-24 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<h1 className="text-4xl">Create an Account.</h1>
|
||||
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
|
||||
{submitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-zinc-500">
|
||||
<p className="m-4 text-s text-contrast">{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
emailError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{!emailError ? (
|
||||
''
|
||||
) : (
|
||||
<p className={`text-red-500 text-xs`}>{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
passwordError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
value={password}
|
||||
minLength={8}
|
||||
required
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{!passwordError ? (
|
||||
''
|
||||
) : (
|
||||
<p className={`text-red-500 text-xs`}>{passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
|
||||
type="submit"
|
||||
>
|
||||
Create Account
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center mt-4">
|
||||
<p className="align-baseline text-sm">
|
||||
Already have an account?
|
||||
<Link className="inline underline" to="/account">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callAccountCreateApi({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/account/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({email, password, firstName, lastName}),
|
||||
});
|
||||
if (res.status === 200) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
error: error.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {Text, Button} from '~/components/elements';
|
||||
import {useRenderServerComponents} from '~/lib/utils';
|
||||
|
||||
export function AccountDeleteAddress({
|
||||
addressId,
|
||||
close,
|
||||
}: {
|
||||
addressId: string;
|
||||
close: () => void;
|
||||
}) {
|
||||
// Necessary for edits to show up on the main page
|
||||
const renderServerComponents = useRenderServerComponents();
|
||||
|
||||
async function deleteAddress(id: string) {
|
||||
const response = await callDeleteAddressApi(id);
|
||||
if (response.error) {
|
||||
alert(response.error);
|
||||
return;
|
||||
}
|
||||
renderServerComponents();
|
||||
close();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text className="mb-4" as="h3" size="lead">
|
||||
Confirm removal
|
||||
</Text>
|
||||
<Text as="p">Are you sure you wish to remove this address?</Text>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
className="text-sm"
|
||||
onClick={() => {
|
||||
deleteAddress(addressId);
|
||||
}}
|
||||
variant="primary"
|
||||
width="full"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
className="text-sm mt-2"
|
||||
onClick={close}
|
||||
variant="secondary"
|
||||
width="full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callDeleteAddressApi(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/account/address/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (_e) {
|
||||
return {
|
||||
error: 'Error removing address. Please try again.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {Seo} from '@shopify/hydrogen';
|
||||
import {useState} from 'react';
|
||||
import {Modal} from '../index';
|
||||
import {AccountDetailsEdit} from './AccountDetailsEdit.client';
|
||||
|
||||
export function AccountDetails({
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
email,
|
||||
}: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const close = () => setIsEditing(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditing ? (
|
||||
<Modal close={close}>
|
||||
<Seo type="noindex" data={{title: 'Account details'}} />
|
||||
<AccountDetailsEdit
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
phone={phone}
|
||||
email={email}
|
||||
close={close}
|
||||
/>
|
||||
</Modal>
|
||||
) : null}
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h3 className="font-bold text-lead">Account Details</h3>
|
||||
<div className="lg:p-8 p-6 border border-gray-200 rounded">
|
||||
<div className="flex">
|
||||
<h3 className="font-bold text-base flex-1">Profile & Security</h3>
|
||||
<button
|
||||
className="underline text-sm font-normal"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-primary/50">Name</div>
|
||||
<p className="mt-1">
|
||||
{firstName || lastName
|
||||
? (firstName ? firstName + ' ' : '') + lastName
|
||||
: 'Add name'}{' '}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 text-sm text-primary/50">Contact</div>
|
||||
<p className="mt-1">{phone ?? 'Add mobile'}</p>
|
||||
|
||||
<div className="mt-4 text-sm text-primary/50">Email address</div>
|
||||
<p className="mt-1">{email}</p>
|
||||
|
||||
<div className="mt-4 text-sm text-primary/50">Password</div>
|
||||
<p className="mt-1">**************</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
import {useState} from 'react';
|
||||
|
||||
import {Text, Button} from '~/components';
|
||||
import {
|
||||
emailValidation,
|
||||
passwordValidation,
|
||||
useRenderServerComponents,
|
||||
} from '~/lib/utils';
|
||||
|
||||
interface FormElements {
|
||||
firstName: HTMLInputElement;
|
||||
lastName: HTMLInputElement;
|
||||
phone: HTMLInputElement;
|
||||
email: HTMLInputElement;
|
||||
currentPassword: HTMLInputElement;
|
||||
newPassword: HTMLInputElement;
|
||||
newPassword2: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function AccountDetailsEdit({
|
||||
firstName: _firstName = '',
|
||||
lastName: _lastName = '',
|
||||
phone: _phone = '',
|
||||
email: _email = '',
|
||||
close,
|
||||
}: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
close: () => void;
|
||||
}) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [firstName, setFirstName] = useState(_firstName);
|
||||
const [lastName, setLastName] = useState(_lastName);
|
||||
const [phone, setPhone] = useState(_phone);
|
||||
const [email, setEmail] = useState(_email);
|
||||
const [emailError, setEmailError] = useState<null | string>(null);
|
||||
const [currentPasswordError, setCurrentPasswordError] = useState<
|
||||
null | string
|
||||
>(null);
|
||||
const [newPasswordError, setNewPasswordError] = useState<null | string>(null);
|
||||
const [newPassword2Error, setNewPassword2Error] = useState<null | string>(
|
||||
null,
|
||||
);
|
||||
const [submitError, setSubmitError] = useState<null | string>(null);
|
||||
|
||||
// Necessary for edits to show up on the main page
|
||||
const renderServerComponents = useRenderServerComponents();
|
||||
|
||||
async function onSubmit(
|
||||
event: React.FormEvent<HTMLFormElement & FormElements>,
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
setEmailError(null);
|
||||
setCurrentPasswordError(null);
|
||||
setNewPasswordError(null);
|
||||
setNewPassword2Error(null);
|
||||
|
||||
const emailError = emailValidation(event.currentTarget.email);
|
||||
if (emailError) {
|
||||
setEmailError(emailError);
|
||||
}
|
||||
|
||||
let currentPasswordError, newPasswordError, newPassword2Error;
|
||||
|
||||
// Only validate the password fields if the current password has a value
|
||||
if (event.currentTarget.currentPassword.value) {
|
||||
currentPasswordError = passwordValidation(
|
||||
event.currentTarget.currentPassword,
|
||||
);
|
||||
if (currentPasswordError) {
|
||||
setCurrentPasswordError(currentPasswordError);
|
||||
}
|
||||
|
||||
newPasswordError = passwordValidation(event.currentTarget.newPassword);
|
||||
if (newPasswordError) {
|
||||
setNewPasswordError(newPasswordError);
|
||||
}
|
||||
|
||||
newPassword2Error =
|
||||
event.currentTarget.newPassword.value !==
|
||||
event.currentTarget.newPassword2.value
|
||||
? 'The two passwords entered did not match'
|
||||
: null;
|
||||
if (newPassword2Error) {
|
||||
setNewPassword2Error(newPassword2Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
emailError ||
|
||||
currentPasswordError ||
|
||||
newPasswordError ||
|
||||
newPassword2Error
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
|
||||
const accountUpdateResponse = await callAccountUpdateApi({
|
||||
email,
|
||||
newPassword: event.currentTarget.newPassword.value,
|
||||
currentPassword: event.currentTarget.currentPassword.value,
|
||||
phone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
|
||||
setSaving(false);
|
||||
|
||||
if (accountUpdateResponse.error) {
|
||||
setSubmitError(accountUpdateResponse.error);
|
||||
return;
|
||||
}
|
||||
|
||||
renderServerComponents();
|
||||
close();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text className="mt-4 mb-6" as="h3" size="lead">
|
||||
Update your profile
|
||||
</Text>
|
||||
<form noValidate onSubmit={onSubmit}>
|
||||
{submitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-red-100 rounded">
|
||||
<p className="m-4 text-sm text-red-900">{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="firstname"
|
||||
name="firstname"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
placeholder="First name"
|
||||
aria-label="First name"
|
||||
value={firstName}
|
||||
onChange={(event) => {
|
||||
setFirstName(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="lastname"
|
||||
name="lastname"
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
placeholder="Last name"
|
||||
aria-label="Last name"
|
||||
value={lastName}
|
||||
onChange={(event) => {
|
||||
setLastName(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
autoComplete="tel"
|
||||
placeholder="Mobile"
|
||||
aria-label="Mobile"
|
||||
value={phone}
|
||||
onChange={(event) => {
|
||||
setPhone(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline rounded ${
|
||||
emailError ? ' border-red-500' : 'border-gray-500'
|
||||
}`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={`text-red-500 text-xs ${!emailError ? 'invisible' : ''}`}
|
||||
>
|
||||
{emailError}
|
||||
</p>
|
||||
</div>
|
||||
<Text className="mb-6 mt-6" as="h3" size="lead">
|
||||
Change your password
|
||||
</Text>
|
||||
<Password
|
||||
name="currentPassword"
|
||||
label="Current password"
|
||||
passwordError={currentPasswordError}
|
||||
/>
|
||||
<Password
|
||||
name="newPassword"
|
||||
label="New password"
|
||||
passwordError={newPasswordError}
|
||||
/>
|
||||
<Password
|
||||
name="newPassword2"
|
||||
label="Re-enter new password"
|
||||
passwordError={newPassword2Error}
|
||||
/>
|
||||
<Text
|
||||
size="fine"
|
||||
color="subtle"
|
||||
className={`mt-1 ${
|
||||
currentPasswordError || newPasswordError ? 'text-red-500' : ''
|
||||
}`}
|
||||
>
|
||||
Passwords must be at least 6 characters.
|
||||
</Text>
|
||||
{newPassword2Error ? <br /> : null}
|
||||
<Text
|
||||
size="fine"
|
||||
className={`mt-1 text-red-500 ${
|
||||
newPassword2Error ? '' : 'invisible'
|
||||
}`}
|
||||
>
|
||||
{newPassword2Error}
|
||||
</Text>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
className="text-sm mb-2"
|
||||
variant="primary"
|
||||
width="full"
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
type="button"
|
||||
className="text-sm"
|
||||
variant="secondary"
|
||||
width="full"
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Password({
|
||||
name,
|
||||
passwordError,
|
||||
label,
|
||||
}: {
|
||||
name: string;
|
||||
passwordError: string | null;
|
||||
label: string;
|
||||
}) {
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<input
|
||||
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline rounded ${
|
||||
passwordError ? ' border-red-500' : 'border-gray-500'
|
||||
}`}
|
||||
id={name}
|
||||
name={name}
|
||||
type="password"
|
||||
autoComplete={
|
||||
name === 'currentPassword' ? 'current-password' : undefined
|
||||
}
|
||||
placeholder={label}
|
||||
aria-label={label}
|
||||
value={password}
|
||||
minLength={8}
|
||||
required
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callAccountUpdateApi({
|
||||
email,
|
||||
phone,
|
||||
firstName,
|
||||
lastName,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
}: {
|
||||
email: string;
|
||||
phone: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/account`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
phone,
|
||||
firstName,
|
||||
lastName,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (_e) {
|
||||
return {
|
||||
error: 'Error saving account. Please try again.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import {useState} from 'react';
|
||||
import {useNavigate, Link} from '@shopify/hydrogen/client';
|
||||
|
||||
interface FormElements {
|
||||
email: HTMLInputElement;
|
||||
password: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function AccountLoginForm({shopName}: {shopName: string}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [hasSubmitError, setHasSubmitError] = useState(false);
|
||||
const [showEmailField, setShowEmailField] = useState(true);
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailError, setEmailError] = useState<null | string>(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState<null | string>(null);
|
||||
|
||||
function onSubmit(event: React.FormEvent<HTMLFormElement & FormElements>) {
|
||||
event.preventDefault();
|
||||
|
||||
setEmailError(null);
|
||||
setHasSubmitError(false);
|
||||
setPasswordError(null);
|
||||
|
||||
if (showEmailField) {
|
||||
checkEmail(event);
|
||||
} else {
|
||||
checkPassword(event);
|
||||
}
|
||||
}
|
||||
|
||||
function checkEmail(event: React.FormEvent<HTMLFormElement & FormElements>) {
|
||||
if (event.currentTarget.email.validity.valid) {
|
||||
setShowEmailField(false);
|
||||
} else {
|
||||
setEmailError('Please enter a valid email');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPassword(
|
||||
event: React.FormEvent<HTMLFormElement & FormElements>,
|
||||
) {
|
||||
const validity = event.currentTarget.password.validity;
|
||||
if (validity.valid) {
|
||||
const response = await callLoginApi({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
setHasSubmitError(true);
|
||||
resetForm();
|
||||
} else {
|
||||
navigate('/account');
|
||||
}
|
||||
} else {
|
||||
setPasswordError(
|
||||
validity.valueMissing
|
||||
? 'Please enter a password'
|
||||
: 'Passwords must be at least 6 characters',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setShowEmailField(true);
|
||||
setEmail('');
|
||||
setEmailError(null);
|
||||
setPassword('');
|
||||
setPasswordError(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center my-24 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<h1 className="text-4xl">Sign in.</h1>
|
||||
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
|
||||
{hasSubmitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-zinc-500">
|
||||
<p className="m-4 text-s text-contrast">
|
||||
Sorry we did not recognize either your email or password. Please
|
||||
try to sign in again or create a new account.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{showEmailField && (
|
||||
<EmailField
|
||||
shopName={shopName}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
emailError={emailError}
|
||||
/>
|
||||
)}
|
||||
{!showEmailField && (
|
||||
<ValidEmail email={email} resetForm={resetForm} />
|
||||
)}
|
||||
{!showEmailField && (
|
||||
<PasswordField
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
passwordError={passwordError}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callLoginApi({
|
||||
email,
|
||||
password,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/account/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({email, password}),
|
||||
});
|
||||
if (res.ok) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
error: error.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function EmailField({
|
||||
email,
|
||||
setEmail,
|
||||
emailError,
|
||||
shopName,
|
||||
}: {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
emailError: null | string;
|
||||
shopName: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
emailError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{!emailError ? (
|
||||
''
|
||||
) : (
|
||||
<p className={`text-red-500 text-xs`}>{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="bg-gray-900 rounded text-contrast py-2 px-4 focus:shadow-outline block w-full"
|
||||
type="submit"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center mt-8 border-t border-gray-300">
|
||||
<p className="align-baseline text-sm mt-6">
|
||||
New to {shopName}?
|
||||
<Link className="inline underline" to="/account/register">
|
||||
Create an account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ValidEmail({
|
||||
email,
|
||||
resetForm,
|
||||
}: {
|
||||
email: string;
|
||||
resetForm: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p>{email}</p>
|
||||
<input
|
||||
className="hidden"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
value={email}
|
||||
readOnly
|
||||
></input>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="inline-block align-baseline text-sm underline"
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Change email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordField({
|
||||
password,
|
||||
setPassword,
|
||||
passwordError,
|
||||
}: {
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
passwordError: null | string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
passwordError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
value={password}
|
||||
minLength={8}
|
||||
required
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{!passwordError ? (
|
||||
''
|
||||
) : (
|
||||
<p className={`text-red-500 text-xs`}> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
|
||||
type="submit"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="flex-1"></div>
|
||||
<Link
|
||||
className="inline-block align-baseline text-sm text-primary/50"
|
||||
to="/account/recover"
|
||||
>
|
||||
Forgot password
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type {Order} from '@shopify/hydrogen/storefront-api-types';
|
||||
import {Button, Text, OrderCard} from '~/components';
|
||||
|
||||
export function AccountOrderHistory({orders}: {orders: Order[]}) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h2 className="font-bold text-lead">Order History</h2>
|
||||
{orders?.length ? <Orders orders={orders} /> : <EmptyOrders />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyOrders() {
|
||||
return (
|
||||
<div>
|
||||
<Text className="mb-1" size="fine" width="narrow" as="p">
|
||||
You haven't placed any orders yet.
|
||||
</Text>
|
||||
<div className="w-48">
|
||||
<Button className="text-sm mt-2 w-full" variant="secondary" to={'/'}>
|
||||
Start Shopping
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Orders({orders}: {orders: Order[]}) {
|
||||
return (
|
||||
<ul className="grid-flow-row grid gap-2 gap-y-6 md:gap-4 lg:gap-6 grid-cols-1 false sm:grid-cols-3">
|
||||
{orders.map((order) => (
|
||||
<OrderCard order={order} key={order.id} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import {useState} from 'react';
|
||||
import {useNavigate} from '@shopify/hydrogen/client';
|
||||
|
||||
interface FormElements {
|
||||
password: HTMLInputElement;
|
||||
passwordConfirm: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function AccountPasswordResetForm({
|
||||
id,
|
||||
resetToken,
|
||||
}: {
|
||||
id: string;
|
||||
resetToken: string;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('');
|
||||
const [passwordConfirmError, setPasswordConfirmError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
function passwordValidation(form: HTMLFormElement & FormElements) {
|
||||
setPasswordError(null);
|
||||
setPasswordConfirmError(null);
|
||||
|
||||
let hasError = false;
|
||||
|
||||
if (!form.password.validity.valid) {
|
||||
hasError = true;
|
||||
setPasswordError(
|
||||
form.password.validity.valueMissing
|
||||
? 'Please enter a password'
|
||||
: 'Passwords must be at least 6 characters',
|
||||
);
|
||||
}
|
||||
|
||||
if (!form.passwordConfirm.validity.valid) {
|
||||
hasError = true;
|
||||
setPasswordConfirmError(
|
||||
form.password.validity.valueMissing
|
||||
? 'Please re-enter a password'
|
||||
: 'Passwords must be at least 6 characters',
|
||||
);
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
hasError = true;
|
||||
setPasswordConfirmError('The two password entered did not match.');
|
||||
}
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
async function onSubmit(
|
||||
event: React.FormEvent<HTMLFormElement & FormElements>,
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
if (passwordValidation(event.currentTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await callPasswordResetApi({
|
||||
id,
|
||||
resetToken,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
setSubmitError(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/account');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center my-24 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<h1 className="text-4xl">Reset Password.</h1>
|
||||
<p className="mt-4">Enter a new password for your account.</p>
|
||||
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
|
||||
{submitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-zinc-500">
|
||||
<p className="m-4 text-s text-contrast">{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
passwordError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
value={password}
|
||||
minLength={8}
|
||||
required
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={`text-red-500 text-xs ${
|
||||
!passwordError ? 'invisible' : ''
|
||||
}`}
|
||||
>
|
||||
{passwordError}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
passwordConfirmError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Re-enter password"
|
||||
aria-label="Re-enter password"
|
||||
value={passwordConfirm}
|
||||
required
|
||||
minLength={8}
|
||||
onChange={(event) => {
|
||||
setPasswordConfirm(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={`text-red-500 text-xs ${
|
||||
!passwordConfirmError ? 'invisible' : ''
|
||||
}`}
|
||||
>
|
||||
{passwordConfirmError}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callPasswordResetApi({
|
||||
id,
|
||||
resetToken,
|
||||
password,
|
||||
}: {
|
||||
id: string;
|
||||
resetToken: string;
|
||||
password: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/account/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({id, resetToken, password}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
error: error.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import {useState} from 'react';
|
||||
|
||||
import {emailValidation} from '~/lib/utils';
|
||||
|
||||
interface FormElements {
|
||||
email: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function AccountRecoverForm() {
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailError, setEmailError] = useState<string | null>(null);
|
||||
|
||||
async function onSubmit(
|
||||
event: React.FormEvent<HTMLFormElement & FormElements>,
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
setEmailError(null);
|
||||
setSubmitError(null);
|
||||
|
||||
const newEmailError = emailValidation(event.currentTarget.email);
|
||||
|
||||
if (newEmailError) {
|
||||
setEmailError(newEmailError);
|
||||
return;
|
||||
}
|
||||
|
||||
await callAccountRecoverApi({
|
||||
email,
|
||||
});
|
||||
|
||||
setEmail('');
|
||||
setSubmitSuccess(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center my-24 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
{submitSuccess ? (
|
||||
<>
|
||||
<h1 className="text-4xl">Request Sent.</h1>
|
||||
<p className="mt-4">
|
||||
If that email address is in our system, you will receive an email
|
||||
with instructions about how to reset your password in a few
|
||||
minutes.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-4xl">Forgot Password.</h1>
|
||||
<p className="mt-4">
|
||||
Enter the email address associated with your account to receive a
|
||||
link to reset your password.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
|
||||
{submitError && (
|
||||
<div className="flex items-center justify-center mb-6 bg-zinc-500">
|
||||
<p className="m-4 text-s text-contrast">{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<input
|
||||
className={`mb-1 rounded appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
|
||||
emailError ? ' border-red-500' : 'border-gray-900'
|
||||
}`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{!emailError ? (
|
||||
''
|
||||
) : (
|
||||
<p className={`text-red-500 text-xs`}>{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
|
||||
type="submit"
|
||||
>
|
||||
Request Reset Link
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function callAccountRecoverApi({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
email: string;
|
||||
password?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/account/recover`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({email, password, firstName, lastName}),
|
||||
});
|
||||
if (res.status === 200) {
|
||||
return {};
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
error: error.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
11
examples/hydrogen/src/components/account/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export {AccountActivateForm} from './AccountActivateForm.client';
|
||||
export {AccountAddressBook} from './AccountAddressBook.client';
|
||||
export {AccountAddressEdit} from './AccountAddressEdit.client';
|
||||
export {AccountCreateForm} from './AccountCreateForm.client';
|
||||
export {AccountDeleteAddress} from './AccountDeleteAddress.client';
|
||||
export {AccountDetails} from './AccountDetails.client';
|
||||
export {AccountDetailsEdit} from './AccountDetailsEdit.client';
|
||||
export {AccountLoginForm} from './AccountLoginForm.client';
|
||||
export {AccountOrderHistory} from './AccountOrderHistory.client';
|
||||
export {AccountPasswordResetForm} from './AccountPasswordResetForm.client';
|
||||
export {AccountRecoverForm} from './AccountRecoverForm.client';
|
||||
38
examples/hydrogen/src/components/cards/ArticleCard.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {Image, Link} from '@shopify/hydrogen';
|
||||
import type {Article} from '@shopify/hydrogen/storefront-api-types';
|
||||
|
||||
export function ArticleCard({
|
||||
blogHandle,
|
||||
article,
|
||||
loading,
|
||||
}: {
|
||||
blogHandle: string;
|
||||
article: Article;
|
||||
loading?: HTMLImageElement['loading'];
|
||||
}) {
|
||||
return (
|
||||
<li key={article.id}>
|
||||
<Link to={`/${blogHandle}/${article.handle}`}>
|
||||
{article.image && (
|
||||
<div className="card-image aspect-[3/2]">
|
||||
<Image
|
||||
alt={article.image.altText || article.title}
|
||||
className="object-cover w-full"
|
||||
data={article.image}
|
||||
height={400}
|
||||
loading={loading}
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
width={600}
|
||||
loaderOptions={{
|
||||
scale: 2,
|
||||
crop: 'center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="mt-4 font-medium">{article.title}</h2>
|
||||
<span className="block mt-1">{article.publishedAt}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Image, Link} from '@shopify/hydrogen';
|
||||
import type {Collection} from '@shopify/hydrogen/storefront-api-types';
|
||||
|
||||
import {Heading} from '~/components';
|
||||
|
||||
export function CollectionCard({
|
||||
collection,
|
||||
loading,
|
||||
}: {
|
||||
collection: Collection;
|
||||
loading?: HTMLImageElement['loading'];
|
||||
}) {
|
||||
return (
|
||||
<Link to={`/collections/${collection.handle}`} className="grid gap-4">
|
||||
<div className="card-image bg-primary/5 aspect-[3/2]">
|
||||
{collection?.image && (
|
||||
<Image
|
||||
alt={`Image of ${collection.title}`}
|
||||
data={collection.image}
|
||||
height={400}
|
||||
sizes="(max-width: 32em) 100vw, 33vw"
|
||||
width={600}
|
||||
widths={[400, 500, 600, 700, 800, 900]}
|
||||
loaderOptions={{
|
||||
scale: 2,
|
||||
crop: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Heading as="h3" size="copy">
|
||||
{collection.title}
|
||||
</Heading>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
87
examples/hydrogen/src/components/cards/OrderCard.client.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import {Image, Link, flattenConnection} from '@shopify/hydrogen';
|
||||
import type {
|
||||
Order,
|
||||
OrderLineItem,
|
||||
} from '@shopify/hydrogen/storefront-api-types';
|
||||
|
||||
import {Heading, Text} from '~/components';
|
||||
import {statusMessage} from '~/lib/utils';
|
||||
|
||||
export function OrderCard({order}: {order: Order}) {
|
||||
if (!order?.id) return null;
|
||||
const legacyOrderId = order!.id!.split('/').pop()!.split('?')[0];
|
||||
const lineItems = flattenConnection<OrderLineItem>(order?.lineItems);
|
||||
|
||||
return (
|
||||
<li className="grid text-center border rounded">
|
||||
<Link
|
||||
className="grid items-center gap-4 p-4 md:gap-6 md:p-6 md:grid-cols-2"
|
||||
to={`/account/orders/${legacyOrderId}`}
|
||||
>
|
||||
{lineItems[0].variant?.image && (
|
||||
<div className="card-image aspect-square bg-primary/5">
|
||||
<Image
|
||||
width={168}
|
||||
height={168}
|
||||
widths={[168]}
|
||||
className="w-full fadeIn cover"
|
||||
alt={lineItems[0].variant?.image?.altText ?? 'Order image'}
|
||||
// @ts-expect-error Stock line item variant image type has `url` as optional
|
||||
data={lineItems[0].variant?.image}
|
||||
loaderOptions={{scale: 2, crop: 'center'}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex-col justify-center text-left ${
|
||||
!lineItems[0].variant?.image && 'md:col-span-2'
|
||||
}`}
|
||||
>
|
||||
<Heading as="h3" format size="copy">
|
||||
{lineItems.length > 1
|
||||
? `${lineItems[0].title} +${lineItems.length - 1} more`
|
||||
: lineItems[0].title}
|
||||
</Heading>
|
||||
<dl className="grid grid-gap-1">
|
||||
<dt className="sr-only">Order ID</dt>
|
||||
<dd>
|
||||
<Text size="fine" color="subtle">
|
||||
Order No. {order.orderNumber}
|
||||
</Text>
|
||||
</dd>
|
||||
<dt className="sr-only">Order Date</dt>
|
||||
<dd>
|
||||
<Text size="fine" color="subtle">
|
||||
{new Date(order.processedAt).toDateString()}
|
||||
</Text>
|
||||
</dd>
|
||||
<dt className="sr-only">Fulfillment Status</dt>
|
||||
<dd className="mt-2">
|
||||
<span
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full ${
|
||||
order.fulfillmentStatus === 'FULFILLED'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-primary/5 text-primary/50'
|
||||
}`}
|
||||
>
|
||||
<Text size="fine">
|
||||
{statusMessage(order.fulfillmentStatus)}
|
||||
</Text>
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="self-end border-t">
|
||||
<Link
|
||||
className="block w-full p-2 text-center"
|
||||
to={`/account/orders/${legacyOrderId}`}
|
||||
>
|
||||
<Text color="subtle" className="ml-3">
|
||||
View Details
|
||||
</Text>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
126
examples/hydrogen/src/components/cards/ProductCard.client.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
flattenConnection,
|
||||
Image,
|
||||
Link,
|
||||
Money,
|
||||
useMoney,
|
||||
} from '@shopify/hydrogen';
|
||||
|
||||
import {Text} from '~/components';
|
||||
import {isDiscounted, isNewArrival} from '~/lib/utils';
|
||||
import {getProductPlaceholder} from '~/lib/placeholders';
|
||||
import type {
|
||||
MoneyV2,
|
||||
Product,
|
||||
ProductVariant,
|
||||
ProductVariantConnection,
|
||||
} from '@shopify/hydrogen/storefront-api-types';
|
||||
|
||||
export function ProductCard({
|
||||
product,
|
||||
label,
|
||||
className,
|
||||
loading,
|
||||
onClick,
|
||||
}: {
|
||||
product: Product;
|
||||
label?: string;
|
||||
className?: string;
|
||||
loading?: HTMLImageElement['loading'];
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
let cardLabel;
|
||||
|
||||
const cardData = product?.variants ? product : getProductPlaceholder();
|
||||
|
||||
const {
|
||||
image,
|
||||
priceV2: price,
|
||||
compareAtPriceV2: compareAtPrice,
|
||||
} = flattenConnection<ProductVariant>(
|
||||
cardData?.variants as ProductVariantConnection,
|
||||
)[0] || {};
|
||||
|
||||
if (label) {
|
||||
cardLabel = label;
|
||||
} else if (isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2)) {
|
||||
cardLabel = 'Sale';
|
||||
} else if (isNewArrival(product.publishedAt)) {
|
||||
cardLabel = 'New';
|
||||
}
|
||||
|
||||
const styles = clsx('grid gap-6', className);
|
||||
|
||||
return (
|
||||
<Link onClick={onClick} to={`/products/${product.handle}`}>
|
||||
<div className={styles}>
|
||||
<div className="card-image aspect-[4/5] bg-primary/5">
|
||||
<Text
|
||||
as="label"
|
||||
size="fine"
|
||||
className="absolute top-0 right-0 m-4 text-right text-notice"
|
||||
>
|
||||
{cardLabel}
|
||||
</Text>
|
||||
{image && (
|
||||
<Image
|
||||
className="aspect-[4/5] w-full object-cover fadeIn"
|
||||
widths={[320]}
|
||||
sizes="320px"
|
||||
loaderOptions={{
|
||||
crop: 'center',
|
||||
scale: 2,
|
||||
width: 320,
|
||||
height: 400,
|
||||
}}
|
||||
// @ts-ignore Stock type has `src` as optional
|
||||
data={image}
|
||||
alt={image.altText || `Picture of ${product.title}`}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Text
|
||||
className="w-full overflow-hidden whitespace-nowrap text-ellipsis "
|
||||
as="h3"
|
||||
>
|
||||
{product.title}
|
||||
</Text>
|
||||
<div className="flex gap-4">
|
||||
<Text className="flex gap-4">
|
||||
<Money withoutTrailingZeros data={price!} />
|
||||
{isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && (
|
||||
<CompareAtPrice
|
||||
className={'opacity-50'}
|
||||
data={compareAtPrice as MoneyV2}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function CompareAtPrice({
|
||||
data,
|
||||
className,
|
||||
}: {
|
||||
data: MoneyV2;
|
||||
className?: string;
|
||||
}) {
|
||||
const {currencyNarrowSymbol, withoutTrailingZerosAndCurrency} =
|
||||
useMoney(data);
|
||||
|
||||
const styles = clsx('strike', className);
|
||||
|
||||
return (
|
||||
<span className={styles}>
|
||||
{currencyNarrowSymbol}
|
||||
{withoutTrailingZerosAndCurrency}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
1
examples/hydrogen/src/components/cards/index.server.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {CollectionCard} from './CollectionCard.server';
|
||||
3
examples/hydrogen/src/components/cards/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export {ArticleCard} from './ArticleCard';
|
||||
export {OrderCard} from './OrderCard.client';
|
||||
export {ProductCard} from './ProductCard.client';
|
||||
100
examples/hydrogen/src/components/cart/CartDetails.client.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import {useRef} from 'react';
|
||||
import {useScroll} from 'react-use';
|
||||
import {
|
||||
useCart,
|
||||
CartLineProvider,
|
||||
CartShopPayButton,
|
||||
Money,
|
||||
} from '@shopify/hydrogen';
|
||||
|
||||
import {Button, Text, CartLineItem, CartEmpty} from '~/components';
|
||||
|
||||
export function CartDetails({
|
||||
layout,
|
||||
onClose,
|
||||
}: {
|
||||
layout: 'drawer' | 'page';
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const {lines} = useCart();
|
||||
const scrollRef = useRef(null);
|
||||
const {y} = useScroll(scrollRef);
|
||||
|
||||
if (lines.length === 0) {
|
||||
return <CartEmpty onClose={onClose} layout={layout} />;
|
||||
}
|
||||
|
||||
const container = {
|
||||
drawer: 'grid grid-cols-1 h-screen-no-nav grid-rows-[1fr_auto]',
|
||||
page: 'pb-12 grid md:grid-cols-2 md:items-start gap-8 md:gap-8 lg:gap-12',
|
||||
};
|
||||
|
||||
const content = {
|
||||
drawer: 'px-6 pb-6 sm-max:pt-2 overflow-auto transition md:px-12',
|
||||
page: 'flex-grow md:translate-y-4',
|
||||
};
|
||||
|
||||
const summary = {
|
||||
drawer: 'grid gap-6 p-6 border-t md:px-12',
|
||||
page: 'sticky top-nav grid gap-6 p-4 md:px-6 md:translate-y-4 bg-primary/5 rounded w-full',
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={container[layout]}>
|
||||
<section
|
||||
ref={scrollRef}
|
||||
aria-labelledby="cart-contents"
|
||||
className={`${content[layout]} ${y > 0 ? 'border-t' : ''}`}
|
||||
>
|
||||
<ul className="grid gap-6 md:gap-10">
|
||||
{lines.map((line) => {
|
||||
return (
|
||||
<CartLineProvider key={line.id} line={line}>
|
||||
<CartLineItem />
|
||||
</CartLineProvider>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
<section aria-labelledby="summary-heading" className={summary[layout]}>
|
||||
<h2 id="summary-heading" className="sr-only">
|
||||
Order summary
|
||||
</h2>
|
||||
<OrderSummary />
|
||||
<CartCheckoutActions />
|
||||
</section>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function CartCheckoutActions() {
|
||||
const {checkoutUrl} = useCart();
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-4">
|
||||
<Button to={checkoutUrl}>Continue to Checkout</Button>
|
||||
<CartShopPayButton />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OrderSummary() {
|
||||
const {cost} = useCart();
|
||||
return (
|
||||
<>
|
||||
<dl className="grid">
|
||||
<div className="flex items-center justify-between font-medium">
|
||||
<Text as="dt">Subtotal</Text>
|
||||
<Text as="dd">
|
||||
{cost?.subtotalAmount?.amount ? (
|
||||
<Money data={cost?.subtotalAmount} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
85
examples/hydrogen/src/components/cart/CartEmpty.client.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import {useRef} from 'react';
|
||||
import {useScroll} from 'react-use';
|
||||
import {fetchSync} from '@shopify/hydrogen';
|
||||
import {Button, Text, ProductCard, Heading, Skeleton} from '~/components';
|
||||
import type {Product} from '@shopify/hydrogen/storefront-api-types';
|
||||
import {Suspense} from 'react';
|
||||
|
||||
export function CartEmpty({
|
||||
onClose,
|
||||
layout = 'drawer',
|
||||
}: {
|
||||
onClose?: () => void;
|
||||
layout?: 'page' | 'drawer';
|
||||
}) {
|
||||
const scrollRef = useRef(null);
|
||||
const {y} = useScroll(scrollRef);
|
||||
|
||||
const container = {
|
||||
drawer: `grid content-start gap-4 px-6 pb-8 transition overflow-y-scroll md:gap-12 md:px-12 h-screen-no-nav md:pb-12 ${
|
||||
y > 0 ? 'border-t' : ''
|
||||
}`,
|
||||
page: `grid pb-12 w-full md:items-start gap-4 md:gap-8 lg:gap-12`,
|
||||
};
|
||||
|
||||
const topProductsContainer = {
|
||||
drawer: '',
|
||||
page: 'md:grid-cols-4 sm:grid-col-4',
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className={container[layout]}>
|
||||
<section className="grid gap-6">
|
||||
<Text format>
|
||||
Looks like you haven’t added anything yet, let’s get you
|
||||
started!
|
||||
</Text>
|
||||
<div>
|
||||
<Button onClick={onClose}>Continue shopping</Button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="grid gap-8 pt-4">
|
||||
<Heading format size="copy">
|
||||
Shop Best Sellers
|
||||
</Heading>
|
||||
<div
|
||||
className={`grid grid-cols-2 gap-x-6 gap-y-8 ${topProductsContainer[layout]}`}
|
||||
>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<TopProducts onClose={onClose} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopProducts({onClose}: {onClose?: () => void}) {
|
||||
const products: Product[] = fetchSync('/api/bestSellers').json();
|
||||
|
||||
if (products.length === 0) {
|
||||
return <Text format>No products found.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{products.map((product) => (
|
||||
<ProductCard product={product} key={product.id} onClick={onClose} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<>
|
||||
{[...new Array(4)].map((_, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={i} className="grid gap-2">
|
||||
<Skeleton className="aspect-[3/4]" />
|
||||
<Skeleton className="w-32 h-4" />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
103
examples/hydrogen/src/components/cart/CartLineItem.client.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
useCart,
|
||||
useCartLine,
|
||||
CartLineQuantityAdjustButton,
|
||||
CartLinePrice,
|
||||
CartLineQuantity,
|
||||
Image,
|
||||
Link,
|
||||
} from '@shopify/hydrogen';
|
||||
import type {Image as ImageType} from '@shopify/hydrogen/storefront-api-types';
|
||||
|
||||
import {Heading, IconRemove, Text} from '~/components';
|
||||
|
||||
export function CartLineItem() {
|
||||
const {linesRemove} = useCart();
|
||||
const {id: lineId, quantity, merchandise} = useCartLine();
|
||||
|
||||
return (
|
||||
<li key={lineId} className="flex gap-4">
|
||||
<div className="flex-shrink">
|
||||
<Image
|
||||
width={112}
|
||||
height={112}
|
||||
widths={[112]}
|
||||
data={merchandise.image as ImageType}
|
||||
loaderOptions={{
|
||||
scale: 2,
|
||||
crop: 'center',
|
||||
}}
|
||||
className="object-cover object-center w-24 h-24 border rounded md:w-28 md:h-28"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between flex-grow">
|
||||
<div className="grid gap-2">
|
||||
<Heading as="h3" size="copy">
|
||||
<Link to={`/products/${merchandise.product.handle}`}>
|
||||
{merchandise.product.title}
|
||||
</Link>
|
||||
</Heading>
|
||||
|
||||
<div className="grid pb-2">
|
||||
{(merchandise?.selectedOptions || []).map((option) => (
|
||||
<Text color="subtle" key={option.name}>
|
||||
{option.name}: {option.value}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex justify-start text-copy">
|
||||
<CartLineQuantityAdjust lineId={lineId} quantity={quantity} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => linesRemove([lineId])}
|
||||
className="flex items-center justify-center w-10 h-10 border rounded"
|
||||
>
|
||||
<span className="sr-only">Remove</span>
|
||||
<IconRemove aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Text>
|
||||
<CartLinePrice as="span" />
|
||||
</Text>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLineQuantityAdjust({
|
||||
lineId,
|
||||
quantity,
|
||||
}: {
|
||||
lineId: string;
|
||||
quantity: number;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={`quantity-${lineId}`} className="sr-only">
|
||||
Quantity, {quantity}
|
||||
</label>
|
||||
<div className="flex items-center border rounded">
|
||||
<CartLineQuantityAdjustButton
|
||||
adjust="decrease"
|
||||
aria-label="Decrease quantity"
|
||||
className="w-10 h-10 transition text-primary/50 hover:text-primary disabled:cursor-wait"
|
||||
>
|
||||
−
|
||||
</CartLineQuantityAdjustButton>
|
||||
<CartLineQuantity as="div" className="px-2 text-center" />
|
||||
<CartLineQuantityAdjustButton
|
||||
adjust="increase"
|
||||
aria-label="Increase quantity"
|
||||
className="w-10 h-10 transition text-primary/50 hover:text-primary disabled:cursor-wait"
|
||||
>
|
||||
+
|
||||
</CartLineQuantityAdjustButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
examples/hydrogen/src/components/cart/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export {CartDetails} from './CartDetails.client';
|
||||
export {CartEmpty} from './CartEmpty.client';
|
||||
export {CartLineItem} from './CartLineItem.client';
|
||||
42
examples/hydrogen/src/components/elements/Button.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import clsx from 'clsx';
|
||||
import {Link} from '@shopify/hydrogen';
|
||||
|
||||
import {missingClass} from '~/lib/utils';
|
||||
|
||||
export function Button({
|
||||
as = 'button',
|
||||
className = '',
|
||||
variant = 'primary',
|
||||
width = 'auto',
|
||||
...props
|
||||
}: {
|
||||
as?: React.ElementType;
|
||||
className?: string;
|
||||
variant?: 'primary' | 'secondary' | 'inline';
|
||||
width?: 'auto' | 'full';
|
||||
[key: string]: any;
|
||||
}) {
|
||||
const Component = props?.to ? Link : as;
|
||||
|
||||
const baseButtonClasses =
|
||||
'inline-block rounded font-medium text-center py-3 px-6';
|
||||
|
||||
const variants = {
|
||||
primary: `${baseButtonClasses} bg-primary text-contrast`,
|
||||
secondary: `${baseButtonClasses} border border-primary/10 bg-contrast text-primary`,
|
||||
inline: 'border-b border-primary/10 leading-none pb-1',
|
||||
};
|
||||
|
||||
const widths = {
|
||||
auto: 'w-auto',
|
||||
full: 'w-full',
|
||||
};
|
||||
|
||||
const styles = clsx(
|
||||
missingClass(className, 'bg-') && variants[variant],
|
||||
missingClass(className, 'w-') && widths[width],
|
||||
className,
|
||||
);
|
||||
|
||||
return <Component className={styles} {...props} />;
|
||||
}
|
||||
44
examples/hydrogen/src/components/elements/Grid.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
export function Grid({
|
||||
as: Component = 'div',
|
||||
className,
|
||||
flow = 'row',
|
||||
gap = 'default',
|
||||
items = 4,
|
||||
layout = 'default',
|
||||
...props
|
||||
}: {
|
||||
as?: React.ElementType;
|
||||
className?: string;
|
||||
flow?: 'row' | 'col';
|
||||
gap?: 'default' | 'blog';
|
||||
items?: number;
|
||||
layout?: 'default' | 'products' | 'auto' | 'blog';
|
||||
[key: string]: any;
|
||||
}) {
|
||||
const layouts = {
|
||||
default: `grid-cols-1 ${items === 2 && 'md:grid-cols-2'} ${
|
||||
items === 3 && 'sm:grid-cols-3'
|
||||
} ${items > 3 && 'md:grid-cols-3'} ${items >= 4 && 'lg:grid-cols-4'}`,
|
||||
products: `grid-cols-2 ${items >= 3 && 'md:grid-cols-3'} ${
|
||||
items >= 4 && 'lg:grid-cols-4'
|
||||
}`,
|
||||
auto: 'auto-cols-auto',
|
||||
blog: `grid-cols-2 pt-24`,
|
||||
};
|
||||
|
||||
const gaps = {
|
||||
default: 'grid gap-2 gap-y-6 md:gap-4 lg:gap-6',
|
||||
blog: 'grid gap-6',
|
||||
};
|
||||
|
||||
const flows = {
|
||||
row: 'grid-flow-row',
|
||||
col: 'grid-flow-col',
|
||||
};
|
||||
|
||||
const styles = clsx(flows[flow], gaps[gap], layouts[layout], className);
|
||||
|
||||
return <Component {...props} className={styles} />;
|
||||
}
|
||||
45
examples/hydrogen/src/components/elements/Heading.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {missingClass, formatText} from '~/lib/utils';
|
||||
|
||||
export function Heading({
|
||||
as: Component = 'h2',
|
||||
children,
|
||||
className = '',
|
||||
format,
|
||||
size = 'heading',
|
||||
width = 'default',
|
||||
...props
|
||||
}: {
|
||||
as?: React.ElementType;
|
||||
children: React.ReactNode;
|
||||
format?: boolean;
|
||||
size?: 'display' | 'heading' | 'lead' | 'copy';
|
||||
width?: 'default' | 'narrow' | 'wide';
|
||||
} & React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
const sizes = {
|
||||
display: 'font-bold text-display',
|
||||
heading: 'font-bold text-heading',
|
||||
lead: 'font-bold text-lead',
|
||||
copy: 'font-medium text-copy',
|
||||
};
|
||||
|
||||
const widths = {
|
||||
default: 'max-w-prose',
|
||||
narrow: 'max-w-prose-narrow',
|
||||
wide: 'max-w-prose-wide',
|
||||
};
|
||||
|
||||
const styles = clsx(
|
||||
missingClass(className, 'whitespace-') && 'whitespace-pre-wrap',
|
||||
missingClass(className, 'max-w-') && widths[width],
|
||||
missingClass(className, 'font-') && sizes[size],
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
<Component {...props} className={styles}>
|
||||
{format ? formatText(children) : children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
236
examples/hydrogen/src/components/elements/Icon.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
type IconProps = JSX.IntrinsicElements['svg'] & {
|
||||
direction?: 'up' | 'right' | 'down' | 'left';
|
||||
};
|
||||
|
||||
function Icon({
|
||||
children,
|
||||
className,
|
||||
fill = 'currentColor',
|
||||
stroke,
|
||||
...props
|
||||
}: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
{...props}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
className={clsx('w-5 h-5', className)}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Accounts</title>
|
||||
<circle cx="20" cy="10.5" r="4.5" strokeWidth="2" />
|
||||
<path
|
||||
d="M20 19C13.4375 19 9.5 20.2857 9.5 28H30.5C30.5 20.2857 26.5625 19 20 19Z"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconMenu(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props} stroke={props.stroke || 'currentColor'}>
|
||||
<title>Menu</title>
|
||||
<line x1="3" y1="6.375" x2="17" y2="6.375" strokeWidth="1.25" />
|
||||
<line x1="3" y1="10.375" x2="17" y2="10.375" strokeWidth="1.25" />
|
||||
<line x1="3" y1="14.375" x2="17" y2="14.375" strokeWidth="1.25" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconClose(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props} stroke={props.stroke || 'currentColor'}>
|
||||
<title>Close</title>
|
||||
<line
|
||||
x1="4.44194"
|
||||
y1="4.30806"
|
||||
x2="15.7556"
|
||||
y2="15.6218"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
<line
|
||||
y1="-0.625"
|
||||
x2="16"
|
||||
y2="-0.625"
|
||||
transform="matrix(-0.707107 0.707107 0.707107 0.707107 16 4.75)"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconArrow({direction = 'right'}: IconProps) {
|
||||
let rotate;
|
||||
|
||||
switch (direction) {
|
||||
case 'right':
|
||||
rotate = 'rotate-0';
|
||||
break;
|
||||
case 'left':
|
||||
rotate = 'rotate-180';
|
||||
break;
|
||||
case 'up':
|
||||
rotate = '-rotate-90';
|
||||
break;
|
||||
case 'down':
|
||||
rotate = 'rotate-90';
|
||||
break;
|
||||
default:
|
||||
rotate = 'rotate-0';
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon className={`w-5 h-5 ${rotate}`}>
|
||||
<title>Arrow</title>
|
||||
<path d="M7 3L14 10L7 17" strokeWidth="1.25" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCaret({
|
||||
direction = 'down',
|
||||
stroke = 'currentColor',
|
||||
...props
|
||||
}: IconProps) {
|
||||
let rotate;
|
||||
|
||||
switch (direction) {
|
||||
case 'down':
|
||||
rotate = 'rotate-0';
|
||||
break;
|
||||
case 'up':
|
||||
rotate = 'rotate-180';
|
||||
break;
|
||||
case 'left':
|
||||
rotate = '-rotate-90';
|
||||
break;
|
||||
case 'right':
|
||||
rotate = 'rotate-90';
|
||||
break;
|
||||
default:
|
||||
rotate = 'rotate-0';
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={`w-5 h-5 transition ${rotate}`}
|
||||
fill="transparent"
|
||||
stroke={stroke}
|
||||
>
|
||||
<title>Caret</title>
|
||||
<path d="M14 8L10 12L6 8" strokeWidth="1.25" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSelect(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Select</title>
|
||||
<path d="M7 8.5L10 6.5L13 8.5" strokeWidth="1.25" />
|
||||
<path d="M13 11.5L10 13.5L7 11.5" strokeWidth="1.25" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBag(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Bag</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.125 5a1.875 1.875 0 0 1 3.75 0v.375h-3.75V5Zm-1.25.375V5a3.125 3.125 0 1 1 6.25 0v.375h3.5V15A2.625 2.625 0 0 1 14 17.625H6A2.625 2.625 0 0 1 3.375 15V5.375h3.5ZM4.625 15V6.625h10.75V15c0 .76-.616 1.375-1.375 1.375H6c-.76 0-1.375-.616-1.375-1.375Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconAccount(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Account</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.9998 12.625c-1.9141 0-3.6628.698-5.0435 1.8611C3.895 13.2935 3.25 11.7221 3.25 10c0-3.728 3.022-6.75 6.75-6.75 3.7279 0 6.75 3.022 6.75 6.75 0 1.7222-.645 3.2937-1.7065 4.4863-1.3807-1.1632-3.1295-1.8613-5.0437-1.8613ZM10 18c-2.3556 0-4.4734-1.0181-5.9374-2.6382C2.7806 13.9431 2 12.0627 2 10c0-4.4183 3.5817-8 8-8s8 3.5817 8 8-3.5817 8-8 8Zm0-12.5c-1.567 0-2.75 1.394-2.75 3s1.183 3 2.75 3 2.75-1.394 2.75-3-1.183-3-2.75-3Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconHelp(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Help</title>
|
||||
<path d="M3.375 10a6.625 6.625 0 1 1 13.25 0 6.625 6.625 0 0 1-13.25 0ZM10 2.125a7.875 7.875 0 1 0 0 15.75 7.875 7.875 0 0 0 0-15.75Zm.699 10.507H9.236V14h1.463v-1.368ZM7.675 7.576A3.256 3.256 0 0 0 7.5 8.67h1.245c0-.496.105-.89.316-1.182.218-.299.553-.448 1.005-.448a1 1 0 0 1 .327.065c.124.044.24.113.35.208.108.095.2.223.272.383.08.154.12.34.12.558a1.3 1.3 0 0 1-.076.471c-.044.131-.11.252-.197.361-.08.102-.174.197-.283.285-.102.087-.212.182-.328.284a3.157 3.157 0 0 0-.382.383c-.102.124-.19.27-.262.438a2.476 2.476 0 0 0-.164.591 6.333 6.333 0 0 0-.043.81h1.179c0-.263.021-.485.065-.668a1.65 1.65 0 0 1 .207-.47c.088-.139.19-.263.306-.372.117-.11.244-.223.382-.34l.35-.306c.116-.11.218-.23.305-.361.095-.139.168-.3.219-.482.058-.19.087-.412.087-.667 0-.35-.062-.664-.186-.942a1.881 1.881 0 0 0-.513-.689 2.07 2.07 0 0 0-.753-.427A2.721 2.721 0 0 0 10.12 6c-.4 0-.764.066-1.092.197a2.36 2.36 0 0 0-.83.536c-.225.234-.4.515-.523.843Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSearch(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Search</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M13.3 8.52a4.77 4.77 0 1 1-9.55 0 4.77 4.77 0 0 1 9.55 0Zm-.98 4.68a6.02 6.02 0 1 1 .88-.88l4.3 4.3-.89.88-4.3-4.3Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCheck({
|
||||
stroke = 'currentColor',
|
||||
...props
|
||||
}: React.ComponentProps<typeof Icon>) {
|
||||
return (
|
||||
<Icon {...props} fill="transparent" stroke={stroke}>
|
||||
<title>Check</title>
|
||||
<circle cx="10" cy="10" r="7.25" strokeWidth="1.25" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="m7.04 10.37 2.42 2.41 3.5-5.56"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconRemove(props: IconProps) {
|
||||
return (
|
||||
<Icon {...props} fill="transparent" stroke={props.stroke || 'currentColor'}>
|
||||
<title>Remove</title>
|
||||
<path
|
||||
d="M4 6H16"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M8.5 9V14" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M11.5 9V14" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path
|
||||
d="M5.5 6L6 17H14L14.5 6"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 6L8 5C8 4 8.75 3 10 3C11.25 3 12 4 12 5V6"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
24
examples/hydrogen/src/components/elements/Input.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
export function Input({
|
||||
className = '',
|
||||
type,
|
||||
variant,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
type?: string;
|
||||
variant: 'search' | 'minisearch';
|
||||
[key: string]: any;
|
||||
}) {
|
||||
const variants = {
|
||||
search:
|
||||
'bg-transparent px-0 py-2 text-heading w-full focus:ring-0 border-x-0 border-t-0 transition border-b-2 border-primary/10 focus:border-primary/90',
|
||||
minisearch:
|
||||
'bg-transparent hidden md:inline-block text-left lg:text-right border-b transition border-transparent -mb-px border-x-0 border-t-0 appearance-none px-0 py-1 focus:ring-transparent placeholder:opacity-20 placeholder:text-inherit',
|
||||
};
|
||||
|
||||
const styles = clsx(variants[variant], className);
|
||||
|
||||
return <input type={type} {...props} className={styles} />;
|
||||
}
|
||||