Compare commits
537 Commits
v3.0.0-alp
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
892b884745 | ||
|
|
e8dd0a7daf | ||
|
|
26151e39c9 | ||
|
|
453e331014 | ||
|
|
7f72006020 | ||
|
|
3c13df3c2d | ||
|
|
d31af813e2 | ||
|
|
a85dc66a39 | ||
|
|
9492f0ae29 | ||
|
|
51149c75ff | ||
|
|
56bedb821f | ||
|
|
d3bca574aa | ||
|
|
c462bf229f | ||
|
|
8a452c42af | ||
|
|
07f2d74dc3 | ||
|
|
2ce65dc1e0 | ||
|
|
37be06448c | ||
|
|
9c13089a2f | ||
|
|
b642cb2d93 | ||
|
|
9e5d521567 | ||
|
|
6eabc99e01 | ||
|
|
ea917dd811 | ||
|
|
070d8e1731 | ||
|
|
664c60d2bc | ||
|
|
e25814e1ee | ||
|
|
27ea117731 | ||
|
|
7ab156e117 | ||
|
|
f2d415663f | ||
|
|
bdf08a19d1 | ||
|
|
cb90e9f622 | ||
|
|
b05a5e1fb6 | ||
|
|
92a5da1006 | ||
|
|
75a95469b2 | ||
|
|
c0ae287d46 | ||
|
|
a2b92aa3ff | ||
|
|
544a2285d3 | ||
|
|
8c39950ea3 | ||
|
|
6d642fe9b9 | ||
|
|
f175a741bc | ||
|
|
3290376f80 | ||
|
|
2b5c1ba99a | ||
|
|
bcb3f08386 | ||
|
|
b729b9bebd | ||
|
|
26ee91eb48 | ||
|
|
43a17f67a0 | ||
|
|
c71d2db949 | ||
|
|
04f1df8af1 | ||
|
|
1c490aee42 | ||
|
|
c6132df866 | ||
|
|
d8f91cc94c | ||
|
|
568b074809 | ||
|
|
401c16e485 | ||
|
|
17bee6a145 | ||
|
|
8829fba6cf | ||
|
|
e8983abe65 | ||
|
|
01f38c4e33 | ||
|
|
10b99ceb6f | ||
|
|
1140426b73 | ||
|
|
5a82f34801 | ||
|
|
5420d889fe | ||
|
|
d9bb51fdc7 | ||
|
|
9a636a3cfb | ||
|
|
181f82f33e | ||
|
|
6a9cde24b0 | ||
|
|
dc31d9c715 | ||
|
|
45b3f06e1b | ||
|
|
d5f7944ac4 | ||
|
|
3d50caf985 | ||
|
|
4d7ef58e7e | ||
|
|
3e117f4e99 | ||
|
|
888d6f8856 | ||
|
|
9ebf8693d4 | ||
|
|
d8c3127b09 | ||
|
|
b6631f4778 | ||
|
|
2e77bdf11e | ||
|
|
cce75f11ca | ||
|
|
d8b6b39dbb | ||
|
|
fa89057aac | ||
|
|
15db0a8018 | ||
|
|
b7a4d9cea4 | ||
|
|
5b676c36e5 | ||
|
|
32231762ff | ||
|
|
a7096c1599 | ||
|
|
bed428c27e | ||
|
|
873e698352 | ||
|
|
ad13577399 | ||
|
|
31a9c77055 | ||
|
|
bae0c2df5f | ||
|
|
0ed31def68 | ||
|
|
0e7a6ad5ab | ||
|
|
180797540c | ||
|
|
c00babf9b3 | ||
|
|
943681ae3c | ||
|
|
f14ce367d2 | ||
|
|
3eb5766323 | ||
|
|
cd5e8d7b52 | ||
|
|
361d12e97c | ||
|
|
fb4a5a3715 | ||
|
|
9c2585ba86 | ||
|
|
feb6296bb4 | ||
|
|
74eb71c304 | ||
|
|
fa2083f764 | ||
|
|
7111834a99 | ||
|
|
a943c7eddb | ||
|
|
2d089a7bae | ||
|
|
5bba969f0d | ||
|
|
3a43fd34c0 | ||
|
|
d9005b3f53 | ||
|
|
fab9e32175 | ||
|
|
e71c1c2ec4 | ||
|
|
81fb0515fb | ||
|
|
739dfc1434 | ||
|
|
a4e8795666 | ||
|
|
14134d637d | ||
|
|
2b698a9018 | ||
|
|
91684c8a7d | ||
|
|
fbdfe1d9dd | ||
|
|
7221725121 | ||
|
|
640348df3a | ||
|
|
4ed99e017a | ||
|
|
df6b9dd30b | ||
|
|
faf142baff | ||
|
|
f80cb9f553 | ||
|
|
d3eaa1fceb | ||
|
|
df77152851 | ||
|
|
937202b27c | ||
|
|
3581f39c31 | ||
|
|
c1d9c81b68 | ||
|
|
20355a4dd4 | ||
|
|
cf66d7f09b | ||
|
|
30afe81462 | ||
|
|
18ee6e8867 | ||
|
|
9f78a93403 | ||
|
|
bd046e2437 | ||
|
|
e9004a93a4 | ||
|
|
4816a1638a | ||
|
|
22c53392a3 | ||
|
|
bdaa9e831d | ||
|
|
036bcd6b8f | ||
|
|
4d2bc861cf | ||
|
|
98722dc0fd | ||
|
|
629d7c3263 | ||
|
|
5f7af5317a | ||
|
|
8bb1b60964 | ||
|
|
7ef5493414 | ||
|
|
a3ac838221 | ||
|
|
14400d1cb9 | ||
|
|
7d531646fd | ||
|
|
6f6c1435c7 | ||
|
|
332b8b6f34 | ||
|
|
d40a734080 | ||
|
|
94f1dfef52 | ||
|
|
0857dbe465 | ||
|
|
71f19fba58 | ||
|
|
24b18fb0fd | ||
|
|
5731241a5c | ||
|
|
47e70abb4e | ||
|
|
0ede95f375 | ||
|
|
b723efdd3b | ||
|
|
14c513690d | ||
|
|
88f239e784 | ||
|
|
a1f6bf8a67 | ||
|
|
912dcd38df | ||
|
|
da5028cdee | ||
|
|
899faa62f1 | ||
|
|
9df6a644c9 | ||
|
|
1a6d9eaa11 | ||
|
|
7d447af277 | ||
|
|
d8baaab849 | ||
|
|
3e1523f007 | ||
|
|
fa38af025f | ||
|
|
6ca9ff847f | ||
|
|
a8824b2b51 | ||
|
|
6aa3752b16 | ||
|
|
c483a439bf | ||
|
|
74bdf1c681 | ||
|
|
7437d9fe58 | ||
|
|
51f7351962 | ||
|
|
d01fcb921b | ||
|
|
6179c938bf | ||
|
|
dbbcb658a9 | ||
|
|
16f97ad7c3 | ||
|
|
bc7445ed99 | ||
|
|
e4d024cd0d | ||
|
|
1005de8295 | ||
|
|
c79289cedf | ||
|
|
6a745be036 | ||
|
|
cee9cc33ed | ||
|
|
9a5e9313cd | ||
|
|
5401af5812 | ||
|
|
6305a1d1c2 | ||
|
|
95b96e3e9e | ||
|
|
95b3f6d40d | ||
|
|
c258a4bef1 | ||
|
|
647544a0c6 | ||
|
|
7e0a2a879c | ||
|
|
471e1388ae | ||
|
|
1da430b042 | ||
|
|
56ac06c563 | ||
|
|
4dec4bb61c | ||
|
|
99a09c49a3 | ||
|
|
88fd46bfea | ||
|
|
8a6603b3d8 | ||
|
|
f6c9f454a5 | ||
|
|
d8a5426c37 | ||
|
|
c9011dcbfd | ||
|
|
43089fd13c | ||
|
|
bb3bd9c395 | ||
|
|
ba423ab424 | ||
|
|
c23984cac3 | ||
|
|
6685a0fa7e | ||
|
|
ac4750d016 | ||
|
|
951e9fd7f2 | ||
|
|
cbd1554589 | ||
|
|
80c545933f | ||
|
|
594f319fc6 | ||
|
|
102feb9576 | ||
|
|
68274d2862 | ||
|
|
8945b7a4fa | ||
|
|
d5ef93b2ba | ||
|
|
cb0f0dba3a | ||
|
|
7b263be01b | ||
|
|
56df60f520 | ||
|
|
d5cbbc472d | ||
|
|
d987e5628a | ||
|
|
1383191f15 | ||
|
|
27297284cf | ||
|
|
3af3a91c87 | ||
|
|
23c5b71f95 | ||
|
|
2ee6a8ec3a | ||
|
|
10819b8693 | ||
|
|
83c617b452 | ||
|
|
4acb133655 | ||
|
|
6e4135e790 | ||
|
|
f0198b62f3 | ||
|
|
3ff8063ab8 | ||
|
|
8d52f1b279 | ||
|
|
24072d222c | ||
|
|
55c59e71da | ||
|
|
62233788e0 | ||
|
|
b297c5499d | ||
|
|
fb7925f272 | ||
|
|
221e873862 | ||
|
|
e7143e02e2 | ||
|
|
a1d68bd951 | ||
|
|
93ee452a2d | ||
|
|
1abaa5fc17 | ||
|
|
999059bc61 | ||
|
|
39ba39c237 | ||
|
|
009e6c2066 | ||
|
|
8bf03ae706 | ||
|
|
a2fe3f66e3 | ||
|
|
6cd5b253f1 | ||
|
|
abf0461d80 | ||
|
|
abca45e152 | ||
|
|
58ea94f6ac | ||
|
|
49cba92fa1 | ||
|
|
42329fc736 | ||
|
|
68dee49501 | ||
|
|
234837ee1d | ||
|
|
0d3554d70a | ||
|
|
7f6c6c4787 | ||
|
|
b6c975bfdc | ||
|
|
eaf5a86121 | ||
|
|
b9a9dad60a | ||
|
|
a2afc38894 | ||
|
|
b80c92ba93 | ||
|
|
6669a2cedb | ||
|
|
7369da3d8d | ||
|
|
697a0f1ecf | ||
|
|
3db0557b07 | ||
|
|
8178d57ab9 | ||
|
|
f1b2f767bb | ||
|
|
f21b394d21 | ||
|
|
4e4ccca02a | ||
|
|
b6578d6447 | ||
|
|
abeb94a53d | ||
|
|
974a74500b | ||
|
|
61dd17ae5e | ||
|
|
2628249a51 | ||
|
|
5f57782199 | ||
|
|
bceb49ee6c | ||
|
|
beeb59f263 | ||
|
|
4150c87be0 | ||
|
|
a394d8211e | ||
|
|
6a162776f2 | ||
|
|
d41bd7b133 | ||
|
|
f3409fab29 | ||
|
|
dd75fbfee2 | ||
|
|
ae2c85f947 | ||
|
|
c0b454a5de | ||
|
|
27754dd0d7 | ||
|
|
18ec830882 | ||
|
|
1ffc0f552e | ||
|
|
07b676ac81 | ||
|
|
da79f09544 | ||
|
|
993b035285 | ||
|
|
3f2df643e7 | ||
|
|
42c7649176 | ||
|
|
2722d2f5ce | ||
|
|
f71e61d7d9 | ||
|
|
73c76cab77 | ||
|
|
43e8a533b7 | ||
|
|
ea4203bb32 | ||
|
|
568b5073c8 | ||
|
|
471e1f4827 | ||
|
|
b9185a6fcd | ||
|
|
80496aa94c | ||
|
|
5fd6e3c1a8 | ||
|
|
54590c1700 | ||
|
|
b1259be8f2 | ||
|
|
cd161e4b16 | ||
|
|
cb4214fe6e | ||
|
|
9d42751a42 | ||
|
|
c2c637b359 | ||
|
|
2f446e11d6 | ||
|
|
4f566b088c | ||
|
|
0d40d87b31 | ||
|
|
70fcd6bf40 | ||
|
|
94c0095b3b | ||
|
|
4328060637 | ||
|
|
d98d0fd5bd | ||
|
|
5db2863d08 | ||
|
|
ff5e438d6d | ||
|
|
bfd5f13ee9 | ||
|
|
8043188f36 | ||
|
|
e3e0998772 | ||
|
|
86adc6f282 | ||
|
|
b51b519d30 | ||
|
|
c70dcb6a59 | ||
|
|
2486c7dba0 | ||
|
|
1456fcdcad | ||
|
|
a216800c72 | ||
|
|
c3d8597c13 | ||
|
|
844663ce1a | ||
|
|
055e6af7b7 | ||
|
|
479e6ecddc | ||
|
|
b9456e8244 | ||
|
|
432dfef435 | ||
|
|
429c6f7a48 | ||
|
|
6d41f6c56d | ||
|
|
20ac2b86cf | ||
|
|
b88455166a | ||
|
|
9b86de1f9d | ||
|
|
1393c72281 | ||
|
|
bcb538aee2 | ||
|
|
01e8f8c649 | ||
|
|
1119cf3af9 | ||
|
|
216934145c | ||
|
|
40f952cac3 | ||
|
|
330e4a7724 | ||
|
|
e676503e02 | ||
|
|
06233fbb2f | ||
|
|
17298695b1 | ||
|
|
c1081ccfe2 | ||
|
|
30da5a8643 | ||
|
|
55bf5436e4 | ||
|
|
a4956dc649 | ||
|
|
512b7bd429 | ||
|
|
5119c51439 | ||
|
|
f3e25f3277 | ||
|
|
1275c70187 | ||
|
|
be69fc448d | ||
|
|
bcccefe98e | ||
|
|
2061f38d9e | ||
|
|
d0869d9087 | ||
|
|
1eabf316d6 | ||
|
|
a0dd750a52 | ||
|
|
2fc9885abc | ||
|
|
14d683fb9a | ||
|
|
e286519cb1 | ||
|
|
b1e78a3562 | ||
|
|
0bc103658a | ||
|
|
9037b9b4fa | ||
|
|
d194493e9a | ||
|
|
aa22344cdb | ||
|
|
736e7b822e | ||
|
|
2ebda95036 | ||
|
|
d8783eaad4 | ||
|
|
cddb08de1a | ||
|
|
d4e5d3df54 | ||
|
|
414b03ce74 | ||
|
|
96dbab8834 | ||
|
|
03a110a750 | ||
|
|
6accc705be | ||
|
|
312dca003b | ||
|
|
4f9fdb6c14 | ||
|
|
94af06466b | ||
|
|
7cf2686097 | ||
|
|
f14883aa11 | ||
|
|
9df8de2386 | ||
|
|
8b2cf4705e | ||
|
|
364e9832ac | ||
|
|
2deeb61f17 | ||
|
|
14498e8a9c | ||
|
|
eb78022387 | ||
|
|
3677a59a78 | ||
|
|
7c1c840a59 | ||
|
|
a73eaf5d37 | ||
|
|
68989a58a8 | ||
|
|
9841731ae7 | ||
|
|
ba7ac5d439 | ||
|
|
7c60772b26 | ||
|
|
1141a5d3af | ||
|
|
6f74fd1f98 | ||
|
|
75873bfcfa | ||
|
|
1faf621f17 | ||
|
|
1d1c73dfcc | ||
|
|
d057ce0a85 | ||
|
|
0ce26d2c08 | ||
|
|
abf285d713 | ||
|
|
c2ee8e3999 | ||
|
|
167ba0c68f | ||
|
|
9ad1cbe920 | ||
|
|
313ea52e3d | ||
|
|
3acfb7a83f | ||
|
|
783dae2bbb | ||
|
|
59681b211b | ||
|
|
98438175cf | ||
|
|
af40302e5f | ||
|
|
ec0e0ae449 | ||
|
|
607ff17033 | ||
|
|
e73e610669 | ||
|
|
30fddde066 | ||
|
|
a56d2842fb | ||
|
|
35f59a47cc | ||
|
|
817d57bd12 | ||
|
|
5826048e7b | ||
|
|
1a975b31cf | ||
|
|
73298a80f0 | ||
|
|
a5d14ef4c1 | ||
|
|
d0c79b65f8 | ||
|
|
2fc50b1a1f | ||
|
|
0ddeedb0b3 | ||
|
|
09c2fb10f3 | ||
|
|
5bfff5b7ba | ||
|
|
702088375c | ||
|
|
ea507fbcc4 | ||
|
|
0ff1e6632b | ||
|
|
996ee47f96 | ||
|
|
3e9bd5bb62 | ||
|
|
ee7221c986 | ||
|
|
318c126ae3 | ||
|
|
2154aea89f | ||
|
|
4d3ad1af35 | ||
|
|
75cab7688f | ||
|
|
5084d6dd97 | ||
|
|
518f80cbb6 | ||
|
|
c9399efa65 | ||
|
|
dd9133659c | ||
|
|
d3016b7eb5 | ||
|
|
69e884f5b7 | ||
|
|
6d122905f4 | ||
|
|
e6e016ac2d | ||
|
|
3e7925e33f | ||
|
|
7a2ccba63c | ||
|
|
be2134eb69 | ||
|
|
95e422b0e1 | ||
|
|
53d9c4ca95 | ||
|
|
30948ab545 | ||
|
|
906df6b401 | ||
|
|
b9c585bab5 | ||
|
|
12203140ad | ||
|
|
0704152e38 | ||
|
|
f582efe98d | ||
|
|
540579f520 | ||
|
|
24c348dc49 | ||
|
|
4b4c245507 | ||
|
|
400f68d1aa | ||
|
|
5bb27ed9cd | ||
|
|
833498c269 | ||
|
|
80507d487b | ||
|
|
08f4ebaaf8 | ||
|
|
4c418525eb | ||
|
|
4962f6c926 | ||
|
|
50f0e9298c | ||
|
|
89efcc5db1 | ||
|
|
c3119a5632 | ||
|
|
069bbd92b0 | ||
|
|
c4422a2593 | ||
|
|
3aab9d368e | ||
|
|
b6afab63b2 | ||
|
|
b93f5e9c44 | ||
|
|
630082035f | ||
|
|
c74e41fc76 | ||
|
|
08ce7c58b5 | ||
|
|
1853fde379 | ||
|
|
f29d22ca95 | ||
|
|
5ea5f928ab | ||
|
|
efd6d35eb3 | ||
|
|
2b2538f13a | ||
|
|
9375dae179 | ||
|
|
674bb3758d | ||
|
|
cedf9a2eb8 | ||
|
|
7d2dc5b6c6 | ||
|
|
9d2aad7bf9 | ||
|
|
f085d7609b | ||
|
|
a49243a42a | ||
|
|
e79f431f14 | ||
|
|
38e5b6e8e3 | ||
|
|
35bdb785c4 | ||
|
|
25cb146fde | ||
|
|
c10f0f4a9e | ||
|
|
3a3a7f6e16 | ||
|
|
1d3b500962 | ||
|
|
f39f95af6d | ||
|
|
6fec2bbe1c | ||
|
|
2412134073 | ||
|
|
136545d1fd | ||
|
|
684c4d2113 | ||
|
|
6624fd0401 | ||
|
|
31502d2da3 | ||
|
|
3ee39ecca3 | ||
|
|
89e7c305e7 | ||
|
|
60dd71c59e | ||
|
|
0936f77930 | ||
|
|
3c09b95a8c | ||
|
|
780f26f135 | ||
|
|
77618674a0 | ||
|
|
c9c89a6005 | ||
|
|
d1276c4299 | ||
|
|
69730b6c95 | ||
|
|
9147d30152 | ||
|
|
6217c70fb5 | ||
|
|
17352c9a56 | ||
|
|
1b6026304f | ||
|
|
91f9973c6c | ||
|
|
ca2acee38a | ||
|
|
3bd455ced2 | ||
|
|
2c25abd143 | ||
|
|
08a9fb8cd7 | ||
|
|
d8a38b4e35 | ||
|
|
e64660f2d8 | ||
|
|
1a11466e69 | ||
|
|
78ab2fbe09 | ||
|
|
0c45d5773a | ||
|
|
f48335444b |
@@ -8,6 +8,7 @@ module.exports = {
|
||||
plugins: ['payload'],
|
||||
rules: {
|
||||
'payload/no-jsx-import-statements': 'warn',
|
||||
'payload/no-relative-monorepo-imports': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
38
.github/CODEOWNERS
vendored
38
.github/CODEOWNERS
vendored
@@ -1,41 +1,33 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Core ###
|
||||
/packages/payload/src/uploads/ @denolfe
|
||||
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
|
||||
### Package Exports ###
|
||||
/**/exports/ @denolfe @jmikrut
|
||||
|
||||
### Adapters ###
|
||||
/packages/db-*/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
/packages/richtext-*/ @AlessioGr
|
||||
|
||||
### Plugins ###
|
||||
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/plugin-cloud*/ @denolfe
|
||||
/packages/plugin-form-builder/ @jacobsfletch
|
||||
/packages/plugin-live-preview*/ @jacobsfletch
|
||||
/packages/plugin-nested-docs/ @jacobsfletch
|
||||
/packages/plugin-redirects/ @jacobsfletch
|
||||
/packages/plugin-search/ @jacobsfletch
|
||||
/packages/plugin-sentry/ @JessChowdhury
|
||||
/packages/plugin-seo/ @jacobsfletch
|
||||
/packages/plugin-stripe/ @jacobsfletch
|
||||
|
||||
### Examples ###
|
||||
/examples/ @jacobsfletch
|
||||
/examples/testing/ @JarrodMFlesch
|
||||
/examples/email/ @JessChowdhury
|
||||
/examples/whitelabel/ @JessChowdhury
|
||||
|
||||
### Templates ###
|
||||
/templates/ @jacobsfletch @denolfe
|
||||
|
||||
### Misc ###
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-config-payload/ @denolfe
|
||||
/packages/payload-admin-bar/ @jacobsfletch
|
||||
/packages/eslint-*/ @denolfe
|
||||
|
||||
### Build Files ###
|
||||
/**/package.json @denolfe
|
||||
|
||||
/tsconfig.json @denolfe
|
||||
/**/tsconfig*.json @denolfe
|
||||
|
||||
/jest.config.js @denolfe
|
||||
/**/jest.config.js @denolfe
|
||||
|
||||
### Root ###
|
||||
/package.json @denolfe
|
||||
/scripts/ @denolfe
|
||||
/.husky/ @denolfe
|
||||
/.vscode/ @denolfe
|
||||
/.github/ @denolfe
|
||||
/.github/CODEOWNERS @denolfe
|
||||
|
||||
250
.github/workflows/main.yml
vendored
250
.github/workflows/main.yml
vendored
@@ -4,7 +4,17 @@ on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
push:
|
||||
branches: ['main', 'alpha']
|
||||
branches: ['main', 'beta']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
@@ -15,6 +25,10 @@ jobs:
|
||||
needs_build: ${{ steps.filter.outputs.needs_build }}
|
||||
templates: ${{ steps.filter.outputs.templates }}
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
@@ -35,7 +49,7 @@ jobs:
|
||||
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
core-build:
|
||||
build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -45,15 +59,19 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 25
|
||||
|
||||
- name: Use Node.js 18
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -61,81 +79,51 @@ jobs:
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 720
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run build:core
|
||||
- run: pnpm run build:all
|
||||
env:
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
|
||||
- name: Cache build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
plugins-build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run build:plugins
|
||||
|
||||
tests-unit:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
if: false # Disable until tests are updated for 3.0
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -147,16 +135,16 @@ jobs:
|
||||
|
||||
tests-int:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database:
|
||||
- mongodb
|
||||
- postgres
|
||||
# - postgres-custom-schema
|
||||
# - postgres-uuid
|
||||
# - supabase
|
||||
- postgres-custom-schema
|
||||
- postgres-uuid
|
||||
- supabase
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@@ -167,19 +155,24 @@ jobs:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -238,7 +231,7 @@ jobs:
|
||||
|
||||
tests-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -246,70 +239,164 @@ jobs:
|
||||
suite:
|
||||
- _community
|
||||
- access-control
|
||||
# - admin
|
||||
- admin
|
||||
- auth
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
# - fields
|
||||
- fields/lexical
|
||||
- fields
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Array
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Lexical
|
||||
- live-preview
|
||||
- localization
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
- uploads
|
||||
|
||||
env:
|
||||
SUITE_NAME: ${{ matrix.suite }}
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Install Playwright
|
||||
run: pnpm exec playwright install --with-deps
|
||||
- name: Start LocalStack
|
||||
run: pnpm docker:start
|
||||
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
|
||||
|
||||
- name: Store Playwright's Version
|
||||
run: |
|
||||
# Extract the version number using a more targeted regex pattern with awk
|
||||
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
|
||||
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Playwright Browsers for Playwright's Version
|
||||
id: cache-playwright-browsers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
|
||||
|
||||
- name: Setup Playwright - Browsers and Dependencies
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Setup Playwright - Dependencies-only
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||
run: pnpm exec playwright install-deps chromium
|
||||
|
||||
- name: E2E Tests
|
||||
run: pnpm test:e2e ${{ matrix.suite }}
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e ${{ matrix.suite }}
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-${{ matrix.suite }}
|
||||
path: test/test-results/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
tests-type-generation:
|
||||
if: false # This should be replaced with gen on a real Payload project
|
||||
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
|
||||
# - uses: daun/playwright-report-summary@v3
|
||||
# with:
|
||||
# report-file: results_${{ matrix.suite }}.json
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
app-build-with-packed:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
- name: Pack and build app
|
||||
run: |
|
||||
set -ex
|
||||
pnpm run script:pack --dest templates/blank-3.0
|
||||
cd templates/blank-3.0
|
||||
cp .env.example .env
|
||||
ls -la
|
||||
pnpm add ./*.tgz
|
||||
pnpm install --ignore-workspace
|
||||
cat package.json
|
||||
pnpm run build
|
||||
|
||||
tests-type-generation:
|
||||
if: false # This should be replaced with gen on a real Payload project
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -333,11 +420,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Use Node.js 18
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
|
||||
103
.github/workflows/pr-title.yml
vendored
Normal file
103
.github/workflows/pr-title.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: pr-title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: lint-pr-title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
types: |
|
||||
build
|
||||
chore
|
||||
ci
|
||||
docs
|
||||
feat
|
||||
fix
|
||||
perf
|
||||
refactor
|
||||
revert
|
||||
style
|
||||
test
|
||||
types
|
||||
scopes: |
|
||||
cpa
|
||||
db-\*
|
||||
db-mongodb
|
||||
db-postgres
|
||||
email-nodemailer
|
||||
eslint
|
||||
graphql
|
||||
live-preview
|
||||
live-preview-react
|
||||
next
|
||||
payload
|
||||
plugin-cloud
|
||||
plugin-cloud-storage
|
||||
plugin-form-builder
|
||||
plugin-nested-docs
|
||||
plugin-redirects
|
||||
plugin-relationship-object-ids
|
||||
plugin-search
|
||||
plugin-sentry
|
||||
plugin-seo
|
||||
plugin-stripe
|
||||
richtext-\*
|
||||
richtext-lexical
|
||||
richtext-slate
|
||||
storage-\*
|
||||
storage-azure
|
||||
storage-gcs
|
||||
storage-vercel-blob
|
||||
storage-s3
|
||||
translations
|
||||
ui
|
||||
templates
|
||||
examples(\/(\w|-)+)?
|
||||
deps
|
||||
|
||||
# Disallow uppercase letters at the beginning of the subject
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
subjectPatternError: |
|
||||
The subject "{subject}" found in the pull request title "{title}"
|
||||
didn't match the configured pattern. Please ensure that the subject
|
||||
doesn't start with an uppercase character.
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
|
||||
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
|
||||
```
|
||||
feat(ui): add Button component
|
||||
^ ^ ^
|
||||
| | |__ Subject
|
||||
| |_______ Scope
|
||||
|____________ Type
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
@@ -1 +1 @@
|
||||
v18.19.1
|
||||
v18.20.2
|
||||
|
||||
@@ -10,3 +10,5 @@
|
||||
**/temp
|
||||
**/docs/**
|
||||
tsconfig.json
|
||||
packages/payload/*.js
|
||||
packages/payload/*.d.ts
|
||||
|
||||
41
.vscode/launch.json
vendored
41
.vscode/launch.json
vendored
@@ -23,6 +23,17 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/loader/init.js",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Loader",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"env": {
|
||||
"LOADER_TEST_FILE_PATH": "./dependency-test.js"
|
||||
// "LOADER_TEST_FILE_PATH": "../fields/config.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js admin",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -30,6 +41,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js auth",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Auth",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev plugin-cloud-storage",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -40,6 +58,13 @@
|
||||
"PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER": "s3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js collections-graphql",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev GraphQL",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -58,36 +83,26 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev versions",
|
||||
"command": "node --no-deprecation test/dev.js versions",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Versions",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev localization",
|
||||
"command": "node --no-deprecation test/dev.js localization",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Localization",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev uploads",
|
||||
"command": "node --no-deprecation test/dev.js uploads",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Uploads",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Fields (Vite)",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -40,5 +40,6 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files.insertFinalNewline": true
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js'
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views/Root/index.js'
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes/index.js'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes/index.js'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts/Root/index.js'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import QueryString from 'qs'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from './serverURL.js'
|
||||
|
||||
export const fetchDoc = async <T>(args: {
|
||||
collection: string
|
||||
depth?: number
|
||||
id?: string
|
||||
slug?: string
|
||||
}): Promise<T> => {
|
||||
const { id, slug, collection, depth = 2 } = args || {}
|
||||
|
||||
const queryString = QueryString.stringify(
|
||||
{
|
||||
...(slug ? { 'where[slug][equals]': slug } : {}),
|
||||
...(depth ? { depth } : {}),
|
||||
},
|
||||
{ addQueryPrefix: true },
|
||||
)
|
||||
|
||||
const doc: T = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}${queryString}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => {
|
||||
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
|
||||
return res?.docs?.[0]
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { PAYLOAD_SERVER_URL } from './serverURL.js'
|
||||
|
||||
export const fetchDocs = async <T>(collection: string): Promise<T[]> => {
|
||||
const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?depth=0&limit=100`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => {
|
||||
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
|
||||
|
||||
return res?.docs
|
||||
})
|
||||
|
||||
return docs
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Footer } from '../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from './serverURL.js'
|
||||
|
||||
export async function fetchFooter(): Promise<Footer> {
|
||||
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
|
||||
|
||||
const footer = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/footer`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Error fetching doc')
|
||||
return res.json()
|
||||
})
|
||||
?.then((res) => {
|
||||
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching footer')
|
||||
return res
|
||||
})
|
||||
|
||||
return footer
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Header } from '../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from './serverURL.js'
|
||||
|
||||
export async function fetchHeader(): Promise<Header> {
|
||||
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
|
||||
|
||||
const header = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/header`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
?.then((res) => {
|
||||
if (!res.ok) throw new Error('Error fetching doc')
|
||||
return res.json()
|
||||
})
|
||||
?.then((res) => {
|
||||
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching header')
|
||||
return res
|
||||
})
|
||||
|
||||
return header
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
module.exports = {
|
||||
// gitRawCommitsOpts: {
|
||||
// from: 'v2.0.9',
|
||||
// path: 'packages/payload',
|
||||
// },
|
||||
// infile: 'CHANGELOG.md',
|
||||
options: {
|
||||
preset: {
|
||||
name: 'conventionalcommits',
|
||||
types: [
|
||||
{ section: 'Features', type: 'feat' },
|
||||
{ section: 'Features', type: 'feature' },
|
||||
{ section: 'Bug Fixes', type: 'fix' },
|
||||
{ section: 'Documentation', type: 'docs' },
|
||||
],
|
||||
},
|
||||
},
|
||||
// outfile: 'NEW.md',
|
||||
writerOpts: {
|
||||
commitGroupsSort: (a, b) => {
|
||||
const groupOrder = ['Features', 'Bug Fixes', 'Documentation']
|
||||
return groupOrder.indexOf(a.title) - groupOrder.indexOf(b.title)
|
||||
},
|
||||
|
||||
// Scoped commits at the end, alphabetical sort
|
||||
commitsSort: (a, b) => {
|
||||
if (a.scope || b.scope) {
|
||||
if (!a.scope) return -1
|
||||
if (!b.scope) return 1
|
||||
return a.scope === b.scope
|
||||
? a.subject.localeCompare(b.subject)
|
||||
: a.scope.localeCompare(b.scope)
|
||||
}
|
||||
|
||||
// Alphabetical sort
|
||||
return a.subject.localeCompare(b.subject)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -639,12 +639,12 @@ export const CustomArrayManager = () => {
|
||||
|
||||
The `useCollapsible` hook allows you to control parent collapsibles:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
|
||||
| **`collapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
|
||||
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
|
||||
| **`toggle`** | Toggles the state of the nearest collapsible |
|
||||
| **`withinCollapsible`** | Determine when you are within another collaspible | |
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
|
||||
| **`isCollapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
|
||||
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
|
||||
| **`toggle`** | Toggles the state of the nearest collapsible |
|
||||
| **`isWithinCollapsible`** | Determine when you are within another collaspible | |
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -654,10 +654,11 @@ import React from 'react'
|
||||
import { useCollapsible } from 'payload/components/utilities'
|
||||
|
||||
const CustomComponent: React.FC = () => {
|
||||
const { collapsed, toggle } = useCollapsible()
|
||||
const { isCollapsed, toggle } = useCollapsible()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="field-type">I am {collapsed ? 'closed' : 'open'}</p>
|
||||
<p className="field-type">I am {isCollapsed ? 'closed' : 'open'}</p>
|
||||
<button onClick={toggle} type="button">
|
||||
Toggle
|
||||
</button>
|
||||
|
||||
@@ -13,23 +13,25 @@ It's often best practice to write your Collections in separate files and then im
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
|
||||
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
|
||||
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| Option | Description |
|
||||
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
|
||||
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
|
||||
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -75,7 +77,6 @@ property on a collection's config.
|
||||
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
|
||||
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
|
||||
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| `enableRichTextLink` | The [Rich Text](/docs/fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
|
||||
@@ -26,6 +26,7 @@ As with Collection configs, it's often best practice to write your Globals in se
|
||||
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`dbName`** | Custom table or collection name for this global depending on the database adapter. Auto-generated from slug if not defined.
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ export default buildConfig({
|
||||
{
|
||||
label: 'Arabic',
|
||||
code: 'ar',
|
||||
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left) when current locale is rtl
|
||||
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left)
|
||||
// when current locale is rtl
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
@@ -134,13 +135,9 @@ to support localization, you need to specify each field that you would like to l
|
||||
```js
|
||||
{
|
||||
name: 'title',
|
||||
type
|
||||
:
|
||||
'text',
|
||||
// highlight-start
|
||||
localized
|
||||
:
|
||||
true,
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
localized: true,
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
@@ -38,12 +38,18 @@ export default buildConfig({
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| Option | Description |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
|
||||
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
|
||||
|
||||
|
||||
### Access to Drizzle
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ The initial request made to Payload will begin a new transaction and attach it t
|
||||
|
||||
```ts
|
||||
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
|
||||
// because req.transactionID is assigned from Payload and passed through, my-slug will only persist if the entire request is successful
|
||||
// because req.transactionID is assigned from Payload and passed through,
|
||||
// my-slug will only persist if the entire request is successful
|
||||
await req.payload.create({
|
||||
req,
|
||||
collection: 'my-slug',
|
||||
|
||||
@@ -45,6 +45,7 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
|
||||
| **`dbName`** | Custom table name for the field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ Blocks are defined as separate configs of their own.
|
||||
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
|
||||
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
|
||||
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. |
|
||||
| **`dbName`** | Custom table name for this block type when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
#### Auto-generated data per block
|
||||
|
||||
@@ -4,7 +4,7 @@ label: JSON
|
||||
order: 50
|
||||
desc: The JSON field type will store any string in the Database. Learn how to use JSON fields, see examples and options.
|
||||
|
||||
keywords: json, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
keywords: json, jsonSchema, schema, validation, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
<Banner>
|
||||
@@ -30,6 +30,7 @@ This field uses the `monaco-react` editor syntax highlighting.
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step)
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
@@ -52,7 +53,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
|
||||
|
||||
### Example
|
||||
|
||||
`collections/ExampleCollection.ts
|
||||
`collections/ExampleCollection.ts`
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
@@ -68,3 +69,67 @@ export const ExampleCollection: CollectionConfig = {
|
||||
],
|
||||
}
|
||||
```
|
||||
### JSON Schema Validation
|
||||
|
||||
Payload JSON fields fully support the [JSON schema](https://json-schema.org/) standard. By providing a schema in your field config, the editor will be guided in the admin UI, getting typeahead for properties and their formats automatically. When the document is saved, the default validation will prevent saving any invalid data in the field according to the schema in your config.
|
||||
|
||||
If you only provide a URL to a schema, Payload will fetch the desired schema if it is publicly available. If not, it is recommended to add the schema directly to your config or import it from another file so that it can be implemented consistently in your project.
|
||||
|
||||
|
||||
#### Local JSON Schema
|
||||
|
||||
`collections/ExampleCollection.ts`
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const ExampleCollection: CollectionConfig = {
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
{
|
||||
name: 'customerJSON', // required
|
||||
type: 'json', // required
|
||||
jsonSchema: {
|
||||
uri: 'a://b/foo.json', // required
|
||||
fileMatch: ['a://b/foo.json'], // required
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {
|
||||
enum: ['bar', 'foobar'],
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
],
|
||||
}
|
||||
// {"foo": "bar"} or {"foo": "foobar"} - ok
|
||||
// Attempting to create {"foo": "not-bar"} will throw an error
|
||||
```
|
||||
|
||||
#### Remote JSON Schema
|
||||
|
||||
`collections/ExampleCollection.ts`
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const ExampleCollection: CollectionConfig = {
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
{
|
||||
name: 'customerJSON', // required
|
||||
type: 'json', // required
|
||||
jsonSchema: {
|
||||
uri: 'https://example.com/customer.schema.json', // required
|
||||
fileMatch: ['https://example.com/customer.schema.json'], // required
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
// If 'https://example.com/customer.schema.json' has a JSON schema
|
||||
// {"foo": "bar"} or {"foo": "foobar"} - ok
|
||||
// Attempting to create {"foo": "not-bar"} will throw an error
|
||||
```
|
||||
|
||||
@@ -20,27 +20,27 @@ keywords: number, fields, config, configuration, documentation, Content Manageme
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
|
||||
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
|
||||
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
|
||||
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
|
||||
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| Option | Description |
|
||||
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
|
||||
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
|
||||
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
|
||||
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
|
||||
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -42,11 +42,12 @@ export const PublicUser: CollectionConfig = {
|
||||
|
||||
**Payload will automatically open up the following queries:**
|
||||
|
||||
| Query Name | Operation |
|
||||
| ------------------ | ------------------- |
|
||||
| **`PublicUser`** | `findByID` |
|
||||
| **`PublicUsers`** | `find` |
|
||||
| **`mePublicUser`** | `me` auth operation |
|
||||
| Query Name | Operation |
|
||||
| ------------------ | ------------------- |
|
||||
| **`PublicUser`** | `findByID` |
|
||||
| **`PublicUsers`** | `find` |
|
||||
| **`countPublicUsers`** | `count` |
|
||||
| **`mePublicUser`** | `me` auth operation |
|
||||
|
||||
**And the following mutations:**
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, us
|
||||
|
||||
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
|
||||
|
||||
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
|
||||
Wiring your front-end into Live Preview is easy. If your front-end application is built with React, Next.js, Vue or Nuxt.js, use the `useLivePreview` hook that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
|
||||
|
||||
By default, all hooks accept the following args:
|
||||
|
||||
@@ -36,6 +36,10 @@ And return the following values:
|
||||
For example, `data?.relatedPosts?.[0]?.title`.
|
||||
</Banner>
|
||||
|
||||
<Banner type="info">
|
||||
It is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
|
||||
</Banner>
|
||||
|
||||
### React
|
||||
|
||||
If your front-end application is built with React or Next.js, you can use the `useLivePreview` hook that Payload provides.
|
||||
@@ -71,11 +75,40 @@ export const PageClient: React.FC<{
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
If is important that the `depth` argument matches exactly with the depth of your initial page
|
||||
request. The depth property is used to populated relationships and uploads beyond their IDs. See
|
||||
[Depth](../getting-started/concepts#depth) for more information.
|
||||
</Banner>
|
||||
### Vue
|
||||
|
||||
If your front-end application is built with Vue 3 or Nuxt 3, you can use the `useLivePreview` composable that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-vue` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-vue
|
||||
```
|
||||
|
||||
Then, use the `useLivePreview` hook in your Vue component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { PageData } from '~/types';
|
||||
import { defineProps } from 'vue';
|
||||
import { useLivePreview } from '@payloadcms/live-preview-vue';
|
||||
|
||||
// Fetch the initial data on the parent component or using async state
|
||||
const props = defineProps<{ initialData: PageData }>();
|
||||
|
||||
// The hook will take over from here and keep the preview in sync with the changes you make.
|
||||
// The `data` property will contain the live data of the document only when viewed from the Preview view of the Admin UI.
|
||||
const { data } = useLivePreview<PageData>({
|
||||
initialData: props.initialData,
|
||||
serverURL: "<PAYLOAD_SERVER_URL>",
|
||||
depth: 2,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ data.title }}</h1>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Building your own hook
|
||||
|
||||
|
||||
@@ -164,6 +164,22 @@ const result = await payload.findByID({
|
||||
})
|
||||
```
|
||||
|
||||
#### Count
|
||||
|
||||
```js
|
||||
// Result will be an object with:
|
||||
// {
|
||||
// totalDocs: 10, // count of the documents satisfies query
|
||||
// }
|
||||
const result = await payload.count({
|
||||
collection: 'posts', // required
|
||||
locale: 'en',
|
||||
where: {}, // pass a `where` query here
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
})
|
||||
```
|
||||
|
||||
#### Update by ID
|
||||
|
||||
```js
|
||||
|
||||
@@ -90,6 +90,19 @@ Note: Collection slugs must be formatted in kebab-case
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "Count",
|
||||
method: "GET",
|
||||
path: "/api/{collection-slug}/count",
|
||||
description: "Count the documents",
|
||||
example: {
|
||||
slug: "count",
|
||||
req: true,
|
||||
res: {
|
||||
totalDocs: 10
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "Create",
|
||||
method: "POST",
|
||||
|
||||
@@ -138,7 +138,7 @@ import { CallToAction } from '../blocks/CallToAction'
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| ------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|--------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
@@ -157,7 +157,8 @@ Here's an overview of all the included features:
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an <hr> element |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
|
||||
## Creating your own, custom Feature
|
||||
@@ -195,7 +196,8 @@ const Pages: CollectionConfig = {
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
// The HTMLConverter Feature is the feature which manages the HTML serializers. If you do not pass any arguments to it, it will use the default serializers.
|
||||
// The HTMLConverter Feature is the feature which manages the HTML serializers.
|
||||
// If you do not pass any arguments to it, it will use the default serializers.
|
||||
HTMLConverterFeature({}),
|
||||
],
|
||||
}),
|
||||
@@ -234,6 +236,19 @@ This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`,
|
||||
|
||||
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
|
||||
|
||||
#### CSS
|
||||
|
||||
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
|
||||
|
||||
Here is some "base" CSS you can use to ensure that nested lists render correctly:
|
||||
|
||||
```css
|
||||
/* Base CSS for Lexical HTML */
|
||||
.nestedListItem, .list-check {
|
||||
list-style-type: none;
|
||||
}
|
||||
```
|
||||
|
||||
#### Creating your own HTML Converter
|
||||
|
||||
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
|
||||
|
||||
@@ -40,21 +40,22 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
|
||||
### Collection Upload Options
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
|
||||
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
|
||||
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
|
||||
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
|
||||
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
|
||||
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| Option | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
|
||||
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
|
||||
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
|
||||
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
|
||||
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
|
||||
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
|
||||
_An asterisk denotes that a property above is required._
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export default {}
|
||||
@@ -1,9 +1,11 @@
|
||||
# Payload Auth Example Front-End
|
||||
|
||||
This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). It demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
This is a [Next.js](https://nextjs.org) [App Router](https://nextjs.org/docs/app) front-end made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). This example demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
|
||||
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-pages).
|
||||
|
||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
@@ -13,9 +15,10 @@ First you'll need a running Payload app. There is one made explicitly for this e
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) for full details.
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function Home() {
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{
|
||||
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_layout.tsx`.'
|
||||
' strategies through http using the REST and GraphQL APIs. To toggle between these two APIs, see `_layout.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -54,7 +54,7 @@ export const RecoverPasswordForm: React.FC = () => {
|
||||
<div className={classes.formWrapper}>
|
||||
<p>
|
||||
{`Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password. To manage your all users, `}
|
||||
how to reset your password. To manage all of your users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
|
||||
2765
examples/auth/next-app/pnpm-lock.yaml
generated
Normal file
2765
examples/auth/next-app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
||||
# Payload Auth Example Front-End
|
||||
|
||||
This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). It demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
This is a [Next.js](https://nextjs.org) [Pages Router](https://nextjs.org/docs/pages) front-end made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). This example demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
|
||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/pages), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-app).
|
||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-app).
|
||||
|
||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -13,9 +15,10 @@ First you'll need a running Payload app. There is one made explicitly for this e
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
4. `pnpm i`, `yarn dev`, or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) for full details.
|
||||
|
||||
2287
examples/auth/next-pages/pnpm-lock.yaml
generated
Normal file
2287
examples/auth/next-pages/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ export default function Home() {
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{
|
||||
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_app.tsx`.'
|
||||
' strategies through HTTP using the REST and GraphQL APIs. To toggle between these two APIs, see `_app.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -53,7 +53,7 @@ const RecoverPassword: React.FC = () => {
|
||||
<div className={classes.formWrapper}>
|
||||
<p>
|
||||
{`Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password. To manage your all users, `}
|
||||
how to reset your password. To manage all of your users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
|
||||
# NOTE: Change port of `PAYLOAD_PUBLIC_SITE_URL` if front-end is running on another server
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-auth
|
||||
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
|
||||
COOKIE_DOMAIN=localhost
|
||||
PAYLOAD_PUBLIC_SEED=true
|
||||
PAYLOAD_DROP_DATABASE=true
|
||||
|
||||
12
examples/auth/payload/.eslintignore
Normal file
12
examples/auth/payload/.eslintignore
Normal file
@@ -0,0 +1,12 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
playwright.config.ts
|
||||
jest.config.js
|
||||
@@ -1,4 +1,15 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
|
||||
ignorePatterns: ['**/payload-types.ts'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,39 +1,103 @@
|
||||
# Payload Auth Example
|
||||
|
||||
The [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview). Follow the [Quick Start](#quick-start) to get up and running quickly. There are various fully working front-ends made explicitly for this example, including:
|
||||
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
|
||||
|
||||
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, there are various fully working, separately running front-ends made explicitly for this example, including:
|
||||
|
||||
- [Next.js App Router](../next-app)
|
||||
- [Next.js Pages Router](../next-pages)
|
||||
|
||||
Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
|
||||
Those applications run directly alongside this one. Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
|
||||
|
||||
To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Quick Start
|
||||
|
||||
To spin up this example locally, follow these steps:
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
1. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
## How it works
|
||||
|
||||
The `users` collection exposes all [auth-related operations](https://payloadcms.com/docs/authentication/operations) needed to create a fully custom workflow on your front-end using the REST or GraphQL APIs, including:
|
||||
### Collections
|
||||
|
||||
- `Me`
|
||||
- `Login`
|
||||
- `Logout`
|
||||
- `Refresh Token`
|
||||
- `Verify Email`
|
||||
- `Unlock`
|
||||
- `Forgot Password`
|
||||
- `Reset Password`
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
|
||||
|
||||
The [`cors`](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [`csrf`](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [`cookies`](https://payloadcms.com/docs/authentication/config#options) settings are also configured to ensure that the admin panel and front-end can communicate with each other securely.
|
||||
- #### Users (Authentication)
|
||||
|
||||
Users are auth-enabled and encompass both admins and regular users based on the value of their `roles` field. Only `admin` users can access your admin panel to manage your content whereas `user` can authenticate on your front-end and access-controlled interfaces. See [Access Control](#access-control) for more details.
|
||||
|
||||
**Local API**
|
||||
|
||||
On the server, Payload provides all operations needed to authenticate users server-side using the Local API. In Next.js that might look something like this:
|
||||
|
||||
```ts
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { getPayloadHMR } from '@payloadcms/next'
|
||||
import config from '../../payload.config'
|
||||
|
||||
export default async function AccountPage({ searchParams }) {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config: configPromise })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
if (!user) {
|
||||
redirect(
|
||||
`/login?error=${encodeURIComponent('You must be logged in to access your account.')}&redirect=/account`,
|
||||
)
|
||||
}
|
||||
|
||||
return ...
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP**
|
||||
|
||||
The `users` collection also opens an http-layer to expose all [auth-related operations](https://payloadcms.com/docs/authentication/operations) through the REST and GraphQL APIs, including:
|
||||
|
||||
- `Me`
|
||||
- `Login`
|
||||
- `Logout`
|
||||
- `Refresh Token`
|
||||
- `Verify Email`
|
||||
- `Unlock`
|
||||
- `Forgot Password`
|
||||
- `Reset Password`
|
||||
|
||||
This might look something like this:
|
||||
|
||||
```ts
|
||||
await fetch('/api/users/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
> NOTE: You can still use the HTTP APIs on the server if you don't have access to the Local API.
|
||||
|
||||
### Security
|
||||
|
||||
The [`cors`](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [`csrf`](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [`cookies`](https://payloadcms.com/docs/authentication/config#options) settings are all configured to ensure that the admin panel and front-end can communicate with each other securely.
|
||||
|
||||
For additional help, see the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
|
||||
|
||||
### Access Control
|
||||
|
||||
@@ -50,7 +114,7 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to create a user with email `demo@payloadcms.com`, password `demo`, the role `admin`.
|
||||
On boot, a seed migration performed to create a user with email `demo@payloadcms.com`, password `demo`, the role `admin`.
|
||||
|
||||
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
|
||||
|
||||
@@ -58,14 +122,17 @@ On boot, a seed script is included to create a user with email `demo@payloadcms.
|
||||
|
||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
||||
|
||||
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
|
||||
1. First invoke the `payload build` script by running `pnpm build`, `yarn build`, or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Then run `pnpm serve`, `yarn serve`, or `npm run serve` to run Node.js in production and serve Payload from the `./build` directory.
|
||||
|
||||
### Deployment
|
||||
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
|
||||
If you are using an integrated Next.js setup, the easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. Otherwise, easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
|
||||
|
||||
## Questions
|
||||
|
||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
5
examples/auth/payload/next-env.d.ts
vendored
Normal file
5
examples/auth/payload/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
8
examples/auth/payload/next.config.mjs
Normal file
8
examples/auth/payload/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig)
|
||||
@@ -1,48 +1,43 @@
|
||||
{
|
||||
"name": "payload-example-auth",
|
||||
"description": "Payload authentication example.",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"description": "Payload authentication example.",
|
||||
"license": "MIT",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.24",
|
||||
"@payloadcms/next": "3.0.0-beta.24",
|
||||
"@payloadcms/richtext-slate": "3.0.0-beta.24",
|
||||
"@payloadcms/ui": "3.0.0-beta.24",
|
||||
"cross-env": "^7.0.3",
|
||||
"next": "14.3.0-canary.7",
|
||||
"payload": "3.0.0-beta.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "^0.0.1",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@payloadcms/eslint-config": "^1.1.1",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@swc/types": "^0.1.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "5.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
6425
examples/auth/payload/pnpm-lock.yaml
generated
Normal file
6425
examples/auth/payload/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
@import '../../_css/type.scss';
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.label {
|
||||
@extend %label;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.appearance--primary {
|
||||
background-color: var(--theme-elevation-1000);
|
||||
color: var(--theme-elevation-0);
|
||||
}
|
||||
|
||||
.appearance--secondary {
|
||||
background-color: transparent;
|
||||
box-shadow: inset 0 0 0 1px var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
.appearance--default {
|
||||
padding: 0;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
invert?: boolean
|
||||
label?: string
|
||||
newTab?: boolean
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
disabled,
|
||||
el: elFromProps = 'link',
|
||||
href,
|
||||
invert,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
invert && classes[`${appearance}--invert`],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
<span className={classes.label}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Element: ElementType = el
|
||||
|
||||
return (
|
||||
<Element
|
||||
className={className}
|
||||
href={href}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
left?: boolean
|
||||
ref?: Ref<HTMLDivElement>
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { children, className, left = true, right = true } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Gutter.displayName = 'Gutter'
|
||||
@@ -0,0 +1,20 @@
|
||||
@use '../../../_css/queries.scss' as *;
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: calc(var(--base) / 4) var(--base);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
opacity: 1;
|
||||
transition: opacity 100ms linear;
|
||||
visibility: visible;
|
||||
|
||||
> * {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { useAuth } from '../../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const HeaderNav: React.FC = () => {
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={[
|
||||
classes.nav,
|
||||
// fade the nav in on user load to avoid flash of content and layout shift
|
||||
// Vercel also does this in their own website header, see https://vercel.com
|
||||
user === undefined && classes.hide,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{user && (
|
||||
<React.Fragment>
|
||||
<Link href="/account">Account</Link>
|
||||
<Link href="/logout">Logout</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!user && (
|
||||
<React.Fragment>
|
||||
<Link href="/login">Login</Link>
|
||||
<Link href="/create-account">Create Account</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@use '../../_css/queries.scss' as *;
|
||||
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--base) / 2) var(--base);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
:global([data-theme="light"]) {
|
||||
.logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderNav } from './Nav'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link className={classes.logo} href="/">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<HeaderNav />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import type { Permissions } from 'payload/auth'
|
||||
import type { PayloadRequestWithData } from 'payload/types'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
|
||||
export const HydrateClientUser: React.FC<{
|
||||
permissions: Permissions
|
||||
user: PayloadRequestWithData['user']
|
||||
}> = ({ permissions, user }) => {
|
||||
const { setPermissions, setUser } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
setUser(user)
|
||||
setPermissions(permissions)
|
||||
}, [user, permissions, setUser, setPermissions])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.inputWrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
font-family: system-ui;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-1000);
|
||||
height: calc(var(--base) * 2);
|
||||
line-height: calc(var(--base) * 2);
|
||||
padding: 0 calc(var(--base) / 2);
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:-webkit-autofill,
|
||||
&:-webkit-autofill:hover,
|
||||
&:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--theme-text);
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--theme-elevation-150) inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.input {
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--theme-error-150);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: small;
|
||||
line-height: 1.25;
|
||||
margin-top: 4px;
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
error: any
|
||||
label: string
|
||||
name: string
|
||||
register: UseFormRegister<FieldValues & any> // eslint-disable-line @typescript-eslint/no-redundant-type-constituents
|
||||
required?: boolean
|
||||
type?: 'email' | 'number' | 'password' | 'text'
|
||||
validate?: (value: string) => boolean | string
|
||||
}
|
||||
|
||||
export const Input: React.FC<Props> = ({
|
||||
name,
|
||||
type = 'text',
|
||||
error,
|
||||
label,
|
||||
register,
|
||||
required,
|
||||
validate,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.inputWrap}>
|
||||
<label className={classes.label} htmlFor="name">
|
||||
{`${label} ${required ? '*' : ''}`}
|
||||
</label>
|
||||
<input
|
||||
className={[classes.input, error && classes.error].filter(Boolean).join(' ')}
|
||||
{...{ type }}
|
||||
{...register(name, {
|
||||
required,
|
||||
validate,
|
||||
...(type === 'email'
|
||||
? {
|
||||
pattern: {
|
||||
message: 'Please enter a valid email',
|
||||
value: /\S[^\s@]*@\S+\.\S+/,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})}
|
||||
/>
|
||||
{error && (
|
||||
<div className={classes.errorMessage}>
|
||||
{!error?.message && error?.type === 'required'
|
||||
? 'This field is required'
|
||||
: error?.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.message {
|
||||
padding: calc(var(--base) / 2) calc(var(--base) / 2);
|
||||
line-height: 1.25;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--theme-warning-500);
|
||||
color: var(--theme-warning-900);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--theme-error-500);
|
||||
color: var(--theme-error-900);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--theme-success-500);
|
||||
color: var(--theme-success-900);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.default {
|
||||
background-color: var(--theme-elevation-900);
|
||||
color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--theme-warning-100);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--theme-error-100);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--theme-success-100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Message: React.FC<{
|
||||
className?: string
|
||||
error?: React.ReactNode
|
||||
message?: React.ReactNode
|
||||
success?: React.ReactNode
|
||||
warning?: React.ReactNode
|
||||
}> = ({ className, error, message, success, warning }) => {
|
||||
const messageToRender = message || error || success || warning
|
||||
|
||||
if (messageToRender) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.message,
|
||||
className,
|
||||
error && classes.error,
|
||||
success && classes.success,
|
||||
warning && classes.warning,
|
||||
!error && !success && !warning && classes.default,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{messageToRender}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import { Message } from '../Message'
|
||||
|
||||
export const RenderParams: React.FC<{
|
||||
className?: string
|
||||
message?: string
|
||||
params?: string[]
|
||||
}> = ({ className, message, params = ['error', 'message', 'success'] }) => {
|
||||
const searchParams = useSearchParams()
|
||||
const paramValues = params.map(param => searchParams.get(param)).filter(Boolean)
|
||||
|
||||
if (paramValues.length) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{paramValues.map(paramValue => (
|
||||
<Message
|
||||
key={paramValue}
|
||||
message={(message || 'PARAM')?.replace('PARAM', paramValue || '')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.richText {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import serialize from './serialize'
|
||||
|
||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serialize(content)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
@@ -0,0 +1,92 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Text } from 'slate'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
[key: string]: unknown
|
||||
children: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: {
|
||||
alt: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactNode[] =>
|
||||
children.map((node, i) => {
|
||||
if (Text.isText(node)) {
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
||||
|
||||
if (node.bold) {
|
||||
text = <strong key={i}>{text}</strong>
|
||||
}
|
||||
|
||||
if (node.code) {
|
||||
text = <code key={i}>{text}</code>
|
||||
}
|
||||
|
||||
if (node.italic) {
|
||||
text = <em key={i}>{text}</em>
|
||||
}
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <Fragment key={i}>{text}</Fragment>
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'li':
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
return (
|
||||
<a href={escapeHTML(node.url)} key={i}>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
}
|
||||
})
|
||||
|
||||
export default serialize
|
||||
117
examples/auth/payload/src/app/(app)/_css/app.scss
Normal file
117
examples/auth/payload/src/app/(app)/_css/app.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
@use './queries.scss' as *;
|
||||
@use './colors.scss' as *;
|
||||
@use './type.scss' as *;
|
||||
@import './theme.scss';
|
||||
|
||||
:root {
|
||||
--base: 24px;
|
||||
--font-body: system-ui;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
|
||||
--gutter-h: 180px;
|
||||
--block-padding: 120px;
|
||||
|
||||
@include large-break {
|
||||
--gutter-h: 144px;
|
||||
--block-padding: 96px;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: 24px;
|
||||
--block-padding: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
@extend %body;
|
||||
background: var(--theme-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
margin: 0;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--theme-success-500);
|
||||
color: var(--color-base-800);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--theme-success-500);
|
||||
color: var(--color-base-800);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@extend %h1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@extend %h2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@extend %h3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@extend %h4;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@extend %h5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@extend %h6;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--base) 0;
|
||||
|
||||
@include mid-break {
|
||||
margin: calc(var(--base) * 0.75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
83
examples/auth/payload/src/app/(app)/_css/colors.scss
Normal file
83
examples/auth/payload/src/app/(app)/_css/colors.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
:root {
|
||||
--color-base-0: rgb(255, 255, 255);
|
||||
--color-base-50: rgb(245, 245, 245);
|
||||
--color-base-100: rgb(235, 235, 235);
|
||||
--color-base-150: rgb(221, 221, 221);
|
||||
--color-base-200: rgb(208, 208, 208);
|
||||
--color-base-250: rgb(195, 195, 195);
|
||||
--color-base-300: rgb(181, 181, 181);
|
||||
--color-base-350: rgb(168, 168, 168);
|
||||
--color-base-400: rgb(154, 154, 154);
|
||||
--color-base-450: rgb(141, 141, 141);
|
||||
--color-base-500: rgb(128, 128, 128);
|
||||
--color-base-550: rgb(114, 114, 114);
|
||||
--color-base-600: rgb(101, 101, 101);
|
||||
--color-base-650: rgb(87, 87, 87);
|
||||
--color-base-700: rgb(74, 74, 74);
|
||||
--color-base-750: rgb(60, 60, 60);
|
||||
--color-base-800: rgb(47, 47, 47);
|
||||
--color-base-850: rgb(34, 34, 34);
|
||||
--color-base-900: rgb(20, 20, 20);
|
||||
--color-base-950: rgb(7, 7, 7);
|
||||
--color-base-1000: rgb(0, 0, 0);
|
||||
|
||||
--color-success-50: rgb(247, 255, 251);
|
||||
--color-success-100: rgb(240, 255, 247);
|
||||
--color-success-150: rgb(232, 255, 243);
|
||||
--color-success-200: rgb(224, 255, 239);
|
||||
--color-success-250: rgb(217, 255, 235);
|
||||
--color-success-300: rgb(209, 255, 230);
|
||||
--color-success-350: rgb(201, 255, 226);
|
||||
--color-success-400: rgb(193, 255, 222);
|
||||
--color-success-450: rgb(186, 255, 218);
|
||||
--color-success-500: rgb(178, 255, 214);
|
||||
--color-success-550: rgb(160, 230, 193);
|
||||
--color-success-600: rgb(142, 204, 171);
|
||||
--color-success-650: rgb(125, 179, 150);
|
||||
--color-success-700: rgb(107, 153, 128);
|
||||
--color-success-750: rgb(89, 128, 107);
|
||||
--color-success-800: rgb(71, 102, 86);
|
||||
--color-success-850: rgb(53, 77, 64);
|
||||
--color-success-900: rgb(36, 51, 43);
|
||||
--color-success-950: rgb(18, 25, 21);
|
||||
|
||||
--color-warning-50: rgb(255, 255, 246);
|
||||
--color-warning-100: rgb(255, 255, 237);
|
||||
--color-warning-150: rgb(254, 255, 228);
|
||||
--color-warning-200: rgb(254, 255, 219);
|
||||
--color-warning-250: rgb(254, 255, 210);
|
||||
--color-warning-300: rgb(254, 255, 200);
|
||||
--color-warning-350: rgb(254, 255, 191);
|
||||
--color-warning-400: rgb(253, 255, 182);
|
||||
--color-warning-450: rgb(253, 255, 173);
|
||||
--color-warning-500: rgb(253, 255, 164);
|
||||
--color-warning-550: rgb(228, 230, 148);
|
||||
--color-warning-600: rgb(202, 204, 131);
|
||||
--color-warning-650: rgb(177, 179, 115);
|
||||
--color-warning-700: rgb(152, 153, 98);
|
||||
--color-warning-750: rgb(127, 128, 82);
|
||||
--color-warning-800: rgb(101, 102, 66);
|
||||
--color-warning-850: rgb(76, 77, 49);
|
||||
--color-warning-900: rgb(51, 51, 33);
|
||||
--color-warning-950: rgb(25, 25, 16);
|
||||
|
||||
--color-error-50: rgb(255, 241, 241);
|
||||
--color-error-100: rgb(255, 226, 228);
|
||||
--color-error-150: rgb(255, 212, 214);
|
||||
--color-error-200: rgb(255, 197, 200);
|
||||
--color-error-250: rgb(255, 183, 187);
|
||||
--color-error-300: rgb(255, 169, 173);
|
||||
--color-error-350: rgb(255, 154, 159);
|
||||
--color-error-400: rgb(255, 140, 145);
|
||||
--color-error-450: rgb(255, 125, 132);
|
||||
--color-error-500: rgb(255, 111, 118);
|
||||
--color-error-550: rgb(230, 100, 106);
|
||||
--color-error-600: rgb(204, 89, 94);
|
||||
--color-error-650: rgb(179, 78, 83);
|
||||
--color-error-700: rgb(153, 67, 71);
|
||||
--color-error-750: rgb(128, 56, 59);
|
||||
--color-error-800: rgb(102, 44, 47);
|
||||
--color-error-850: rgb(77, 33, 35);
|
||||
--color-error-900: rgb(51, 22, 24);
|
||||
--color-error-950: rgb(25, 11, 12);
|
||||
}
|
||||
1
examples/auth/payload/src/app/(app)/_css/common.scss
Normal file
1
examples/auth/payload/src/app/(app)/_css/common.scss
Normal file
@@ -0,0 +1 @@
|
||||
@forward './queries.scss';
|
||||
28
examples/auth/payload/src/app/(app)/_css/queries.scss
Normal file
28
examples/auth/payload/src/app/(app)/_css/queries.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
$breakpoint-xs-width: 400px;
|
||||
$breakpoint-s-width: 768px;
|
||||
$breakpoint-m-width: 1024px;
|
||||
$breakpoint-l-width: 1440px;
|
||||
|
||||
@mixin extra-small-break {
|
||||
@media (max-width: #{$breakpoint-xs-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin small-break {
|
||||
@media (max-width: #{$breakpoint-s-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mid-break {
|
||||
@media (max-width: #{$breakpoint-m-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin large-break {
|
||||
@media (max-width: #{$breakpoint-l-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
241
examples/auth/payload/src/app/(app)/_css/theme.scss
Normal file
241
examples/auth/payload/src/app/(app)/_css/theme.scss
Normal file
@@ -0,0 +1,241 @@
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--theme-success-50: var(--color-success-50);
|
||||
--theme-success-100: var(--color-success-100);
|
||||
--theme-success-150: var(--color-success-150);
|
||||
--theme-success-200: var(--color-success-200);
|
||||
--theme-success-250: var(--color-success-250);
|
||||
--theme-success-300: var(--color-success-300);
|
||||
--theme-success-350: var(--color-success-350);
|
||||
--theme-success-400: var(--color-success-400);
|
||||
--theme-success-450: var(--color-success-450);
|
||||
--theme-success-500: var(--color-success-500);
|
||||
--theme-success-550: var(--color-success-550);
|
||||
--theme-success-600: var(--color-success-600);
|
||||
--theme-success-650: var(--color-success-650);
|
||||
--theme-success-700: var(--color-success-700);
|
||||
--theme-success-750: var(--color-success-750);
|
||||
--theme-success-800: var(--color-success-800);
|
||||
--theme-success-850: var(--color-success-850);
|
||||
--theme-success-900: var(--color-success-900);
|
||||
--theme-success-950: var(--color-success-950);
|
||||
|
||||
--theme-warning-50: var(--color-warning-50);
|
||||
--theme-warning-100: var(--color-warning-100);
|
||||
--theme-warning-150: var(--color-warning-150);
|
||||
--theme-warning-200: var(--color-warning-200);
|
||||
--theme-warning-250: var(--color-warning-250);
|
||||
--theme-warning-300: var(--color-warning-300);
|
||||
--theme-warning-350: var(--color-warning-350);
|
||||
--theme-warning-400: var(--color-warning-400);
|
||||
--theme-warning-450: var(--color-warning-450);
|
||||
--theme-warning-500: var(--color-warning-500);
|
||||
--theme-warning-550: var(--color-warning-550);
|
||||
--theme-warning-600: var(--color-warning-600);
|
||||
--theme-warning-650: var(--color-warning-650);
|
||||
--theme-warning-700: var(--color-warning-700);
|
||||
--theme-warning-750: var(--color-warning-750);
|
||||
--theme-warning-800: var(--color-warning-800);
|
||||
--theme-warning-850: var(--color-warning-850);
|
||||
--theme-warning-900: var(--color-warning-900);
|
||||
--theme-warning-950: var(--color-warning-950);
|
||||
|
||||
--theme-error-50: var(--color-error-50);
|
||||
--theme-error-100: var(--color-error-100);
|
||||
--theme-error-150: var(--color-error-150);
|
||||
--theme-error-200: var(--color-error-200);
|
||||
--theme-error-250: var(--color-error-250);
|
||||
--theme-error-300: var(--color-error-300);
|
||||
--theme-error-350: var(--color-error-350);
|
||||
--theme-error-400: var(--color-error-400);
|
||||
--theme-error-450: var(--color-error-450);
|
||||
--theme-error-500: var(--color-error-500);
|
||||
--theme-error-550: var(--color-error-550);
|
||||
--theme-error-600: var(--color-error-600);
|
||||
--theme-error-650: var(--color-error-650);
|
||||
--theme-error-700: var(--color-error-700);
|
||||
--theme-error-750: var(--color-error-750);
|
||||
--theme-error-800: var(--color-error-800);
|
||||
--theme-error-850: var(--color-error-850);
|
||||
--theme-error-900: var(--color-error-900);
|
||||
--theme-error-950: var(--color-error-950);
|
||||
|
||||
--theme-elevation-0: var(--color-base-0);
|
||||
--theme-elevation-50: var(--color-base-50);
|
||||
--theme-elevation-100: var(--color-base-100);
|
||||
--theme-elevation-150: var(--color-base-150);
|
||||
--theme-elevation-200: var(--color-base-200);
|
||||
--theme-elevation-250: var(--color-base-250);
|
||||
--theme-elevation-300: var(--color-base-300);
|
||||
--theme-elevation-350: var(--color-base-350);
|
||||
--theme-elevation-400: var(--color-base-400);
|
||||
--theme-elevation-450: var(--color-base-450);
|
||||
--theme-elevation-500: var(--color-base-500);
|
||||
--theme-elevation-550: var(--color-base-550);
|
||||
--theme-elevation-600: var(--color-base-600);
|
||||
--theme-elevation-650: var(--color-base-650);
|
||||
--theme-elevation-700: var(--color-base-700);
|
||||
--theme-elevation-750: var(--color-base-750);
|
||||
--theme-elevation-800: var(--color-base-800);
|
||||
--theme-elevation-850: var(--color-base-850);
|
||||
--theme-elevation-900: var(--color-base-900);
|
||||
--theme-elevation-950: var(--color-base-950);
|
||||
--theme-elevation-1000: var(--color-base-1000);
|
||||
|
||||
--theme-bg: var(--theme-elevation-0);
|
||||
--theme-input-bg: var(--theme-elevation-50);
|
||||
--theme-text: var(--theme-elevation-750);
|
||||
--theme-border-color: var(--theme-elevation-150);
|
||||
|
||||
color-scheme: light;
|
||||
color: var(--theme-text);
|
||||
|
||||
--highlight-default-bg-color: var(--theme-success-400);
|
||||
--highlight-default-text-color: var(--theme-text);
|
||||
|
||||
--highlight-danger-bg-color: var(--theme-error-150);
|
||||
--highlight-danger-text-color: var(--theme-text);
|
||||
}
|
||||
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a,
|
||||
h4 a,
|
||||
h5 a,
|
||||
h6 a {
|
||||
color: var(--theme-elevation-750);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: var(--theme-elevation-750);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--theme-elevation-0: var(--color-base-1000);
|
||||
--theme-elevation-50: var(--color-base-950);
|
||||
--theme-elevation-100: var(--color-base-900);
|
||||
--theme-elevation-150: var(--color-base-850);
|
||||
--theme-elevation-200: var(--color-base-800);
|
||||
--theme-elevation-250: var(--color-base-750);
|
||||
--theme-elevation-300: var(--color-base-700);
|
||||
--theme-elevation-350: var(--color-base-650);
|
||||
--theme-elevation-400: var(--color-base-600);
|
||||
--theme-elevation-450: var(--color-base-550);
|
||||
--theme-elevation-500: var(--color-base-500);
|
||||
--theme-elevation-550: var(--color-base-450);
|
||||
--theme-elevation-600: var(--color-base-400);
|
||||
--theme-elevation-650: var(--color-base-350);
|
||||
--theme-elevation-700: var(--color-base-300);
|
||||
--theme-elevation-750: var(--color-base-250);
|
||||
--theme-elevation-800: var(--color-base-200);
|
||||
--theme-elevation-850: var(--color-base-150);
|
||||
--theme-elevation-900: var(--color-base-100);
|
||||
--theme-elevation-950: var(--color-base-50);
|
||||
--theme-elevation-1000: var(--color-base-0);
|
||||
|
||||
--theme-success-50: var(--color-success-950);
|
||||
--theme-success-100: var(--color-success-900);
|
||||
--theme-success-150: var(--color-success-850);
|
||||
--theme-success-200: var(--color-success-800);
|
||||
--theme-success-250: var(--color-success-750);
|
||||
--theme-success-300: var(--color-success-700);
|
||||
--theme-success-350: var(--color-success-650);
|
||||
--theme-success-400: var(--color-success-600);
|
||||
--theme-success-450: var(--color-success-550);
|
||||
--theme-success-500: var(--color-success-500);
|
||||
--theme-success-550: var(--color-success-450);
|
||||
--theme-success-600: var(--color-success-400);
|
||||
--theme-success-650: var(--color-success-350);
|
||||
--theme-success-700: var(--color-success-300);
|
||||
--theme-success-750: var(--color-success-250);
|
||||
--theme-success-800: var(--color-success-200);
|
||||
--theme-success-850: var(--color-success-150);
|
||||
--theme-success-900: var(--color-success-100);
|
||||
--theme-success-950: var(--color-success-50);
|
||||
|
||||
--theme-warning-50: var(--color-warning-950);
|
||||
--theme-warning-100: var(--color-warning-900);
|
||||
--theme-warning-150: var(--color-warning-850);
|
||||
--theme-warning-200: var(--color-warning-800);
|
||||
--theme-warning-250: var(--color-warning-750);
|
||||
--theme-warning-300: var(--color-warning-700);
|
||||
--theme-warning-350: var(--color-warning-650);
|
||||
--theme-warning-400: var(--color-warning-600);
|
||||
--theme-warning-450: var(--color-warning-550);
|
||||
--theme-warning-500: var(--color-warning-500);
|
||||
--theme-warning-550: var(--color-warning-450);
|
||||
--theme-warning-600: var(--color-warning-400);
|
||||
--theme-warning-650: var(--color-warning-350);
|
||||
--theme-warning-700: var(--color-warning-300);
|
||||
--theme-warning-750: var(--color-warning-250);
|
||||
--theme-warning-800: var(--color-warning-200);
|
||||
--theme-warning-850: var(--color-warning-150);
|
||||
--theme-warning-900: var(--color-warning-100);
|
||||
--theme-warning-950: var(--color-warning-50);
|
||||
|
||||
--theme-error-50: var(--color-error-950);
|
||||
--theme-error-100: var(--color-error-900);
|
||||
--theme-error-150: var(--color-error-850);
|
||||
--theme-error-200: var(--color-error-800);
|
||||
--theme-error-250: var(--color-error-750);
|
||||
--theme-error-300: var(--color-error-700);
|
||||
--theme-error-350: var(--color-error-650);
|
||||
--theme-error-400: var(--color-error-600);
|
||||
--theme-error-450: var(--color-error-550);
|
||||
--theme-error-500: var(--color-error-500);
|
||||
--theme-error-550: var(--color-error-450);
|
||||
--theme-error-600: var(--color-error-400);
|
||||
--theme-error-650: var(--color-error-350);
|
||||
--theme-error-700: var(--color-error-300);
|
||||
--theme-error-750: var(--color-error-250);
|
||||
--theme-error-800: var(--color-error-200);
|
||||
--theme-error-850: var(--color-error-150);
|
||||
--theme-error-900: var(--color-error-100);
|
||||
--theme-error-950: var(--color-error-50);
|
||||
|
||||
--theme-bg: var(--theme-elevation-100);
|
||||
--theme-text: var(--theme-elevation-900);
|
||||
--theme-input-bg: var(--theme-elevation-150);
|
||||
--theme-border-color: var(--theme-elevation-250);
|
||||
|
||||
color-scheme: dark;
|
||||
color: var(--theme-text);
|
||||
|
||||
--highlight-default-bg-color: var(--theme-success-100);
|
||||
--highlight-default-text-color: var(--theme-success-600);
|
||||
|
||||
--highlight-danger-bg-color: var(--theme-error-100);
|
||||
--highlight-danger-text-color: var(--theme-error-550);
|
||||
}
|
||||
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a,
|
||||
h4 a,
|
||||
h5 a,
|
||||
h6 a {
|
||||
color: var(--theme-success-600);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-success-400);
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: var(--theme-success-700);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-success-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
examples/auth/payload/src/app/(app)/_providers/Auth/gql.ts
Normal file
34
examples/auth/payload/src/app/(app)/_providers/Auth/gql.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const USER = `
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const gql = async (query: string): Promise<any> => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const { data, errors } = await res.json()
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message)
|
||||
}
|
||||
|
||||
if (res.ok && data) {
|
||||
return data
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
186
examples/auth/payload/src/app/(app)/_providers/Auth/index.tsx
Normal file
186
examples/auth/payload/src/app/(app)/_providers/Auth/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
'use client'
|
||||
|
||||
import type { Permissions } from 'payload/auth'
|
||||
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
import type { AuthContext, Create, ForgotPassword, Login, Logout, ResetPassword } from './types'
|
||||
|
||||
import { USER, gql } from './gql'
|
||||
import { rest } from './rest'
|
||||
|
||||
const Context = createContext({} as AuthContext)
|
||||
|
||||
export const AuthProvider: React.FC<{ api?: 'gql' | 'rest'; children: React.ReactNode }> = ({
|
||||
api = 'rest',
|
||||
children,
|
||||
}) => {
|
||||
const [user, setUser] = useState<User | null>()
|
||||
const [permissions, setPermissions] = useState<Permissions | null>(null)
|
||||
|
||||
const create = useCallback<Create>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { createUser: user } = await gql(`mutation {
|
||||
createUser(data: { email: "${args.email}", password: "${args.password}", firstName: "${args.firstName}", lastName: "${args.lastName}" }) {
|
||||
${USER}
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const login = useCallback<Login>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/login`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { loginUser } = await gql(`mutation {
|
||||
loginUser(email: "${args.email}", password: "${args.password}") {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
exp
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(loginUser?.user)
|
||||
return loginUser?.user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const logout = useCallback<Logout>(async () => {
|
||||
if (api === 'rest') {
|
||||
await rest(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/logout`)
|
||||
setUser(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
await gql(`mutation {
|
||||
logoutUser
|
||||
}`)
|
||||
|
||||
setUser(null)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
const fetchMe = async () => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/me`,
|
||||
{},
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
setUser(user)
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { meUser } = await gql(`query {
|
||||
meUser {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
exp
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(meUser.user)
|
||||
}
|
||||
}
|
||||
|
||||
void fetchMe()
|
||||
}, [api])
|
||||
|
||||
const forgotPassword = useCallback<ForgotPassword>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/forgot-password`,
|
||||
args,
|
||||
)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { forgotPasswordUser } = await gql(`mutation {
|
||||
forgotPasswordUser(email: "${args.email}")
|
||||
}`)
|
||||
|
||||
return forgotPasswordUser
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const resetPassword = useCallback<ResetPassword>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/reset-password`,
|
||||
args,
|
||||
)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { resetPasswordUser } = await gql(`mutation {
|
||||
resetPasswordUser(password: "${args.password}", token: "${args.token}") {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(resetPasswordUser.user)
|
||||
return resetPasswordUser.user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
create,
|
||||
forgotPassword,
|
||||
login,
|
||||
logout,
|
||||
permissions,
|
||||
resetPassword,
|
||||
setPermissions,
|
||||
setUser,
|
||||
user,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type UseAuth<T = User> = () => AuthContext // eslint-disable-line no-unused-vars
|
||||
|
||||
export const useAuth: UseAuth = () => useContext(Context)
|
||||
34
examples/auth/payload/src/app/(app)/_providers/Auth/rest.ts
Normal file
34
examples/auth/payload/src/app/(app)/_providers/Auth/rest.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { User } from '../../../../payload-types'
|
||||
|
||||
export const rest = async (
|
||||
url: string,
|
||||
args?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
options?: RequestInit,
|
||||
): Promise<User | null | undefined> => {
|
||||
const method = options?.method || 'POST'
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
...(method === 'POST' ? { body: JSON.stringify(args) } : {}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
|
||||
const { errors, user } = await res.json()
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message)
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
return user
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
35
examples/auth/payload/src/app/(app)/_providers/Auth/types.ts
Normal file
35
examples/auth/payload/src/app/(app)/_providers/Auth/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Permissions } from 'payload/auth'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export type ResetPassword = (args: {
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
token: string
|
||||
}) => Promise<User>
|
||||
|
||||
export type ForgotPassword = (args: { email: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Create = (args: {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password: string
|
||||
}) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Login = (args: { email: string; password: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Logout = () => Promise<void>
|
||||
|
||||
export interface AuthContext {
|
||||
create: Create
|
||||
forgotPassword: ForgotPassword
|
||||
login: Login
|
||||
logout: Logout
|
||||
permissions?: Permissions | null
|
||||
resetPassword: ResetPassword
|
||||
setPermissions: (permissions: Permissions | null) => void
|
||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
||||
user?: User | null
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: flex-start;
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.changePassword {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: calc(var(--base) / 2);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
name: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
||||
export const AccountForm: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const { setUser, user } = useAuth()
|
||||
const [changePassword, setChangePassword] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
formState: { errors, isLoading },
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const password = useRef({})
|
||||
password.current = watch('password', '')
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
if (user) {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/${user.id}`, {
|
||||
// Make sure to include cookies with fetch
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json()
|
||||
setUser(json.doc)
|
||||
setSuccess('Successfully updated account.')
|
||||
setError('')
|
||||
setChangePassword(false)
|
||||
reset({
|
||||
name: json.doc.name,
|
||||
email: json.doc.email,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
})
|
||||
} else {
|
||||
setError('There was a problem updating your account.')
|
||||
}
|
||||
}
|
||||
},
|
||||
[user, setUser, reset],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (user === null) {
|
||||
router.push(`/login?unauthorized=account`)
|
||||
}
|
||||
|
||||
// Once user is loaded, reset form to have default values
|
||||
if (user) {
|
||||
reset({
|
||||
email: user.email,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
})
|
||||
}
|
||||
}, [user, router, reset, changePassword])
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Message className={classes.message} error={error} success={success} />
|
||||
{!changePassword ? (
|
||||
<Fragment>
|
||||
<p>
|
||||
{'To change your password, '}
|
||||
<button
|
||||
className={classes.changePassword}
|
||||
onClick={() => setChangePassword(!changePassword)}
|
||||
type="button"
|
||||
>
|
||||
click here
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<p>
|
||||
{'Change your password below, or '}
|
||||
<button
|
||||
className={classes.changePassword}
|
||||
onClick={() => setChangePassword(!changePassword)}
|
||||
type="button"
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
error={errors.passwordConfirm}
|
||||
label="Confirm Password"
|
||||
name="passwordConfirm"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
validate={value => value === password.current || 'The passwords do not match'}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
label={isLoading ? 'Processing' : changePassword ? 'Change password' : 'Update account'}
|
||||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.account {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
|
||||
.params {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
44
examples/auth/payload/src/app/(app)/account/page.tsx
Normal file
44
examples/auth/payload/src/app/(app)/account/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Button } from '../_components/Button'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { HydrateClientUser } from '../_components/HydrateClientUser'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { AccountForm } from './AccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Account() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
if (!user) {
|
||||
redirect(
|
||||
`/login?error=${encodeURIComponent('You must be logged in to access your account.')}&redirect=/account`,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateClientUser permissions={permissions} user={user} />
|
||||
<Gutter className={classes.account}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Account</h1>
|
||||
<p>
|
||||
{`This is your account dashboard. Here you can update your account information and more. To manage all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<AccountForm />
|
||||
<Button appearance="secondary" href="/logout" label="Log out" />
|
||||
</Gutter>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: flex-start;
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
||||
export const CreateAccountForm: React.FC = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<null | string>(null)
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
register,
|
||||
watch,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const password = useRef({})
|
||||
password.current = watch('password', '')
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users`, {
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = response.statusText || 'There was an error creating the account.'
|
||||
setError(message)
|
||||
return
|
||||
}
|
||||
|
||||
const redirect = searchParams.get('redirect')
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setLoading(true)
|
||||
}, 1000)
|
||||
|
||||
try {
|
||||
await login(data)
|
||||
clearTimeout(timer)
|
||||
if (redirect) router.push(redirect)
|
||||
else router.push(`/account?success=${encodeURIComponent('Account created successfully')}`)
|
||||
} catch (_) {
|
||||
clearTimeout(timer)
|
||||
setError('There was an error with the credentials provided. Please try again.')
|
||||
}
|
||||
},
|
||||
[login, router, searchParams],
|
||||
)
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<p>
|
||||
{`This is where new customers can signup and create a new account. To manage all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Message className={classes.message} error={error} />
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
error={errors.passwordConfirm}
|
||||
label="Confirm Password"
|
||||
name="passwordConfirm"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
validate={value => value === password.current || 'The passwords do not match'}
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
label={loading ? 'Processing' : 'Create Account'}
|
||||
type="submit"
|
||||
/>
|
||||
<div>
|
||||
{'Already have an account? '}
|
||||
<Link href={`/login${allParams}`}>Login</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.createAccount {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
32
examples/auth/payload/src/app/(app)/create-account/page.tsx
Normal file
32
examples/auth/payload/src/app/(app)/create-account/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { CreateAccountForm } from './CreateAccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function CreateAccount() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
redirect(
|
||||
`/account?message=${encodeURIComponent(
|
||||
'Cannot create a new account while logged in, please log out and try again.',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.createAccount}>
|
||||
<h1>Create Account</h1>
|
||||
<RenderParams />
|
||||
<CreateAccountForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
29
examples/auth/payload/src/app/(app)/layout.tsx
Normal file
29
examples/auth/payload/src/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Header } from './_components/Header'
|
||||
import './_css/app.scss'
|
||||
import { AuthProvider } from './_providers/Auth'
|
||||
|
||||
export const metadata = {
|
||||
description: 'An example of how to authenticate with Payload from a Next.js app.',
|
||||
title: 'Payload Auth + Next.js App Router Example',
|
||||
}
|
||||
|
||||
export default function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AuthProvider
|
||||
// To toggle between the REST and GraphQL APIs,
|
||||
// change the `api` prop to either `rest` or `gql`
|
||||
api="rest" // change this to `gql` to use the GraphQL API
|
||||
>
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: flex-start;
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export const LoginForm: React.FC = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
||||
const redirect = useRef(searchParams.get('redirect'))
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
const [error, setError] = React.useState<null | string>(null)
|
||||
|
||||
const {
|
||||
formState: { errors, isLoading },
|
||||
handleSubmit,
|
||||
register,
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await login(data)
|
||||
if (redirect?.current) router.push(redirect.current)
|
||||
else router.push('/account')
|
||||
} catch (_) {
|
||||
setError('There was an error with the credentials provided. Please try again.')
|
||||
}
|
||||
},
|
||||
[login, router],
|
||||
)
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<p>
|
||||
{'To log in, use the email '}
|
||||
<b>demo@payloadcms.com</b>
|
||||
{' with the password '}
|
||||
<b>demo</b>
|
||||
{'. To manage your users, '}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Message className={classes.message} error={error} />
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
disabled={isLoading}
|
||||
label={isLoading ? 'Processing' : 'Login'}
|
||||
type="submit"
|
||||
/>
|
||||
<div>
|
||||
<Link href={`/create-account${allParams}`}>Create an account</Link>
|
||||
<br />
|
||||
<Link href={`/recover-password${allParams}`}>Recover your password</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.login {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
|
||||
.params {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
28
examples/auth/payload/src/app/(app)/login/page.tsx
Normal file
28
examples/auth/payload/src/app/(app)/login/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { LoginForm } from './LoginForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Login() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
redirect(`/account?message=${encodeURIComponent('You are already logged in.')}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.login}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Log in</h1>
|
||||
<LoginForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
|
||||
export const LogoutPage: React.FC = () => {
|
||||
const { logout } = useAuth()
|
||||
const [success, setSuccess] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const performLogout = async () => {
|
||||
try {
|
||||
await logout()
|
||||
setSuccess('Logged out successfully.')
|
||||
} catch (_) {
|
||||
setError('You are already logged out.')
|
||||
}
|
||||
}
|
||||
|
||||
void performLogout()
|
||||
}, [logout])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{(error || success) && (
|
||||
<div>
|
||||
<h1>{error || success}</h1>
|
||||
<p>
|
||||
{'What would you like to do next? '}
|
||||
<Link href="/">Click here</Link>
|
||||
{` to go to the home page. To log back in, `}
|
||||
<Link href="/login">click here</Link>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.logout {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user