Compare commits
526 Commits
cojson@0.1
...
cojson@0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c936c8c611 | ||
|
|
58c6013770 | ||
|
|
3eb3291a97 | ||
|
|
6b659f2df3 | ||
|
|
dcc9c9a5ec | ||
|
|
fe9a244363 | ||
|
|
9440bbc058 | ||
|
|
1c92cc2997 | ||
|
|
33ebbf0bdd | ||
|
|
d630b5bde5 | ||
|
|
1c6ae12cd9 | ||
|
|
21bcaabd5a | ||
|
|
17b4d5b668 | ||
|
|
3cd15862d5 | ||
|
|
b3d1ad7201 | ||
|
|
d87df11795 | ||
|
|
82c2a62b2a | ||
|
|
0a9112506e | ||
|
|
fbc29f2f17 | ||
|
|
f6361ee43b | ||
|
|
726dbfb6df | ||
|
|
267f689f10 | ||
|
|
893ad3ae23 | ||
|
|
f5590b1be8 | ||
|
|
17a01f57e8 | ||
|
|
7318d86f52 | ||
|
|
1c8403e87a | ||
|
|
dd747c068a | ||
|
|
1f0f230fe2 | ||
|
|
da655cbff5 | ||
|
|
02f6c6220e | ||
|
|
0755cd198e | ||
|
|
c4a8227b66 | ||
|
|
86f0302233 | ||
|
|
165a6170cd | ||
|
|
5148419df9 | ||
|
|
fc0ecb0968 | ||
|
|
802b5a3060 | ||
|
|
e47af262b3 | ||
|
|
e98b610fd0 | ||
|
|
b554983558 | ||
|
|
d95dcbe7db | ||
|
|
f9d538f049 | ||
|
|
93e68c62f5 | ||
|
|
dadee9dcc5 | ||
|
|
6724c4bd83 | ||
|
|
1942bd5de4 | ||
|
|
16764f6365 | ||
|
|
b56cfc2e1f | ||
|
|
7091bcf9c0 | ||
|
|
436cbfa095 | ||
|
|
104e664bbb | ||
|
|
f199b451eb | ||
|
|
70bc48458e | ||
|
|
f28b2a6135 | ||
|
|
55b770b7c9 | ||
|
|
e6838dfb98 | ||
|
|
5e34061fdc | ||
|
|
acecffaeb2 | ||
|
|
0a98d6aaf2 | ||
|
|
4ea1a63a0a | ||
|
|
41a4c3bc95 | ||
|
|
60d0027f9d | ||
|
|
748c2ff751 | ||
|
|
70938b0ab3 | ||
|
|
f2f5b55dbf | ||
|
|
3c3acae803 | ||
|
|
896ee3460f | ||
|
|
9b9bf44e2b | ||
|
|
392aa88d95 | ||
|
|
7ce82cd934 | ||
|
|
0c8158b91c | ||
|
|
5a48c9c44c | ||
|
|
25c56146f5 | ||
|
|
c564fbb02e | ||
|
|
12481e14c2 | ||
|
|
fd2d247ff5 | ||
|
|
9e9ea029b2 | ||
|
|
a0da272dcd | ||
|
|
72fbcc3262 | ||
|
|
f4c8cc858b | ||
|
|
0ab4d7a20d | ||
|
|
5c98ff4e4f | ||
|
|
4cbda689c4 | ||
|
|
771b0ed914 | ||
|
|
79913c3136 | ||
|
|
43d3511d15 | ||
|
|
928ef14086 | ||
|
|
048dd7def0 | ||
|
|
51fcb8a44b | ||
|
|
c5888c39f5 | ||
|
|
2defcfae67 | ||
|
|
873b146d15 | ||
|
|
213de11c3b | ||
|
|
1b881cc89f | ||
|
|
af295d816a | ||
|
|
fe8d3497c0 | ||
|
|
c2899e94ca | ||
|
|
f4be67e9b6 | ||
|
|
ba9ad295b6 | ||
|
|
9ed5a96ef8 | ||
|
|
4272ea9019 | ||
|
|
9509307ed1 | ||
|
|
be08921bc5 | ||
|
|
ab1798c7bd | ||
|
|
26ae69a242 | ||
|
|
25be055a51 | ||
|
|
21ad3767b9 | ||
|
|
a9383516c1 | ||
|
|
bffc516c68 | ||
|
|
9e7c0d9887 | ||
|
|
99b44d5780 | ||
|
|
02db5f3b1d | ||
|
|
1949a5fcd9 | ||
|
|
dcd3b022cc | ||
|
|
a7b837c7e1 | ||
|
|
88ebcf58ab | ||
|
|
b173e0884a | ||
|
|
f379a168be | ||
|
|
bde6ac7d45 | ||
|
|
231947c97a | ||
|
|
d1609cdd55 | ||
|
|
d5b57ad1fc | ||
|
|
b71ab3168a | ||
|
|
0c8f6e5039 | ||
|
|
0bf5c53bec | ||
|
|
e7b1550003 | ||
|
|
6a93a1b8a3 | ||
|
|
9f654a2603 | ||
|
|
dbf735d9e1 | ||
|
|
c62abefb66 | ||
|
|
1453869a46 | ||
|
|
239da90c9f | ||
|
|
972791e7a8 | ||
|
|
0c0178764e | ||
|
|
928350b821 | ||
|
|
be3fd9c696 | ||
|
|
269c028df0 | ||
|
|
e4df837138 | ||
|
|
54fe6d93ba | ||
|
|
979689c6d8 | ||
|
|
859a37868f | ||
|
|
57bd32d77e | ||
|
|
e21cbccd4b | ||
|
|
a66ab7d174 | ||
|
|
78e91f4030 | ||
|
|
7a915c198e | ||
|
|
c9b0420746 | ||
|
|
2303f3e70a | ||
|
|
a7bc9569a3 | ||
|
|
f351ba0fcd | ||
|
|
d3e554f491 | ||
|
|
b5e31456ad | ||
|
|
42d07ba7b4 | ||
|
|
b81b6ba69b | ||
|
|
1bc1759bb4 | ||
|
|
225bc1f63f | ||
|
|
5d94564f99 | ||
|
|
9633d0187f | ||
|
|
b82ecaa3ca | ||
|
|
111ec8d351 | ||
|
|
512aacdbc2 | ||
|
|
7ad843aa3e | ||
|
|
071128339b | ||
|
|
688ced499d | ||
|
|
ac91c8e7c2 | ||
|
|
c3b303c310 | ||
|
|
fc027a56db | ||
|
|
959a7a3927 | ||
|
|
2548085b59 | ||
|
|
b27bb3e65b | ||
|
|
937284f7e9 | ||
|
|
e999727c70 | ||
|
|
2197766624 | ||
|
|
d1efde468f | ||
|
|
4d4fd0beaa | ||
|
|
2b61e853a7 | ||
|
|
6f79b45544 | ||
|
|
2e1ff99579 | ||
|
|
7361854ee4 | ||
|
|
4a775fada3 | ||
|
|
3fe53a3a4a | ||
|
|
fe37516786 | ||
|
|
4beafb7cf3 | ||
|
|
82a592e08a | ||
|
|
4c6926153a | ||
|
|
c51b088243 | ||
|
|
867cb6b7a5 | ||
|
|
3a1fdd7600 | ||
|
|
3fdbb43b54 | ||
|
|
02969ee89b | ||
|
|
9b4988a514 | ||
|
|
8aa4b59d49 | ||
|
|
ac782674de | ||
|
|
5eb406d54d | ||
|
|
a3be832414 | ||
|
|
7ca8dd960e | ||
|
|
62c8aff73f | ||
|
|
7731109a28 | ||
|
|
0401fcf2a8 | ||
|
|
139a649279 | ||
|
|
9acccb5df2 | ||
|
|
fd90cdb49a | ||
|
|
4a29999c6a | ||
|
|
df487d5335 | ||
|
|
1efe84c691 | ||
|
|
73b99c6c1a | ||
|
|
039b92c839 | ||
|
|
618af5f1e3 | ||
|
|
dfc4286694 | ||
|
|
970ff0d813 | ||
|
|
65eee0ef01 | ||
|
|
eee221f563 | ||
|
|
063553090e | ||
|
|
97f6bcedbd | ||
|
|
7c63e6bb0f | ||
|
|
08aedcf517 | ||
|
|
3e12ee127f | ||
|
|
2283d375ef | ||
|
|
202e763380 | ||
|
|
52bbdb37a9 | ||
|
|
96f743b2f4 | ||
|
|
f5c47feeb6 | ||
|
|
6dffe73bd2 | ||
|
|
6f9ee31179 | ||
|
|
52f324ffc4 | ||
|
|
2d86f53575 | ||
|
|
68cb357a94 | ||
|
|
56ccf9ab9d | ||
|
|
b8b0851433 | ||
|
|
2bbb07b0bf | ||
|
|
4f7bc91502 | ||
|
|
d3053955d8 | ||
|
|
f61a120560 | ||
|
|
2f1307a0ba | ||
|
|
fa15ea56d1 | ||
|
|
1e58ecb3ac | ||
|
|
ceeabfaf89 | ||
|
|
70ce7c5736 | ||
|
|
f40484eca9 | ||
|
|
d581a59aa1 | ||
|
|
0ca09f75c1 | ||
|
|
e8fcd101f2 | ||
|
|
cf43fa7529 | ||
|
|
df1cdda4e8 | ||
|
|
be46042cdc | ||
|
|
6afdb16739 | ||
|
|
7a60d7bb76 | ||
|
|
f8263a8358 | ||
|
|
f6da966922 | ||
|
|
b0b2b85a6f | ||
|
|
8a2ab51543 | ||
|
|
28c19c134f | ||
|
|
0924c9baaa | ||
|
|
8bfaa0a18b | ||
|
|
b2712e18a2 | ||
|
|
66894b63d7 | ||
|
|
147be76399 | ||
|
|
36770bed52 | ||
|
|
466e6c44ee | ||
|
|
5bd8277161 | ||
|
|
b1a05143e3 | ||
|
|
fb761ce66d | ||
|
|
07a6c340dc | ||
|
|
8b4261f7d8 | ||
|
|
0ec917e453 | ||
|
|
6326d0fc45 | ||
|
|
d746b1279a | ||
|
|
0fea904dd0 | ||
|
|
373aef313f | ||
|
|
c09dcdfc76 | ||
|
|
a584590ed8 | ||
|
|
0a830e29a9 | ||
|
|
4402c553b6 | ||
|
|
e76fe343da | ||
|
|
dc183a19b2 | ||
|
|
fef55a4cd6 | ||
|
|
ddef54048f | ||
|
|
a2626a0f38 | ||
|
|
ec579bcaf7 | ||
|
|
8aa2d2a789 | ||
|
|
a39d009b87 | ||
|
|
6b662b0efe | ||
|
|
efff4d0f4f | ||
|
|
ea2b01d8a2 | ||
|
|
e9af90c841 | ||
|
|
2b7c6f5aa7 | ||
|
|
d73a3d9d46 | ||
|
|
8af39077a3 | ||
|
|
54bd487818 | ||
|
|
f01dab5c8f | ||
|
|
a8b3ec7bb0 | ||
|
|
a7f6870048 | ||
|
|
3b294f6994 | ||
|
|
a420b43029 | ||
|
|
a57268de32 | ||
|
|
6b2c4ed280 | ||
|
|
8d4e0027be | ||
|
|
a4141da1b7 | ||
|
|
c9ca5202f9 | ||
|
|
7b50a2e06d | ||
|
|
43dabccb57 | ||
|
|
b6d04f56ef | ||
|
|
628195b678 | ||
|
|
9a5d769717 | ||
|
|
e30a3f66bf | ||
|
|
6327fce933 | ||
|
|
a650da4184 | ||
|
|
6e4a94f6ce | ||
|
|
b73bec64bc | ||
|
|
50ae2f47c2 | ||
|
|
724d8e7f30 | ||
|
|
7b285ab110 | ||
|
|
01ac9b8c4c | ||
|
|
4e2e1ac73e | ||
|
|
94960c1f65 | ||
|
|
b5af58347b | ||
|
|
46a84558c5 | ||
|
|
f93566c045 | ||
|
|
d97ed603a3 | ||
|
|
8d33103182 | ||
|
|
aaa1ff978b | ||
|
|
82655ea7a7 | ||
|
|
8afe3a2e02 | ||
|
|
ae2adcbd15 | ||
|
|
eb0460d330 | ||
|
|
55cb83e6e0 | ||
|
|
6290088fec | ||
|
|
b9c17b37db | ||
|
|
6c76ff8fbf | ||
|
|
3c6a2a6092 | ||
|
|
e8a950e61a | ||
|
|
e2cbf035de | ||
|
|
47599b6307 | ||
|
|
901d0762ee | ||
|
|
d1c1b0c5cc | ||
|
|
cf4ad7285d | ||
|
|
255a947ea6 | ||
|
|
530a263d35 | ||
|
|
2983c7bd58 | ||
|
|
745020b7a8 | ||
|
|
ab6328f767 | ||
|
|
e0555debde | ||
|
|
247f4556e7 | ||
|
|
7903c737f4 | ||
|
|
6145da5525 | ||
|
|
fc0a2e77a3 | ||
|
|
334fbbbb7f | ||
|
|
eaac1e6580 | ||
|
|
114898d8a9 | ||
|
|
991aebf7a7 | ||
|
|
cbc3f0cc65 | ||
|
|
29c487e288 | ||
|
|
0b0590a364 | ||
|
|
1eb01997d8 | ||
|
|
0dc8d511a1 | ||
|
|
9b75880b10 | ||
|
|
962213c712 | ||
|
|
427df8fcbb | ||
|
|
98fe72ed42 | ||
|
|
1f5c81c2ea | ||
|
|
c40aad55dc | ||
|
|
fc41aa165b | ||
|
|
dfca5926de | ||
|
|
9815ec61f0 | ||
|
|
fca60d213e | ||
|
|
b4fdab475b | ||
|
|
2b043abffa | ||
|
|
958c122c36 | ||
|
|
5842838371 | ||
|
|
e136e1b696 | ||
|
|
2475a46578 | ||
|
|
41466ea399 | ||
|
|
44f653a64b | ||
|
|
f8437042a6 | ||
|
|
3b91594d10 | ||
|
|
acd908fbc2 | ||
|
|
4e61d1d191 | ||
|
|
db23582b4c | ||
|
|
4b0b6d8a69 | ||
|
|
d450b394fa | ||
|
|
0abc96e400 | ||
|
|
7562354b29 | ||
|
|
6c085a3919 | ||
|
|
6afff848bc | ||
|
|
47059845cc | ||
|
|
a1735a8232 | ||
|
|
1f5750d8c4 | ||
|
|
f756ce26b5 | ||
|
|
84f5bdda74 | ||
|
|
ee7aefa97c | ||
|
|
b0895981ba | ||
|
|
94f636b2ee | ||
|
|
331ab070f6 | ||
|
|
13e73adfb9 | ||
|
|
265a6405af | ||
|
|
9f6079b6c6 | ||
|
|
4033d78fa6 | ||
|
|
83af94c850 | ||
|
|
70fe856713 | ||
|
|
42e4afc42b | ||
|
|
0e6797b222 | ||
|
|
3634eaf8e9 | ||
|
|
58dfda3d0f | ||
|
|
d304b0bcb5 | ||
|
|
44f5a3f5a2 | ||
|
|
ebb3ce1c25 | ||
|
|
a67bba0dcf | ||
|
|
4a72c26e42 | ||
|
|
084cb5936d | ||
|
|
f1552b8262 | ||
|
|
6826ad8e45 | ||
|
|
7c1b757b62 | ||
|
|
326e1734a4 | ||
|
|
0cf027c91b | ||
|
|
e358881b76 | ||
|
|
cee8010918 | ||
|
|
cc877139ef | ||
|
|
56a9b89538 | ||
|
|
199c463e28 | ||
|
|
50e523d19c | ||
|
|
796ea24288 | ||
|
|
c8be86e823 | ||
|
|
41b7054aab | ||
|
|
101adcd024 | ||
|
|
a2854aeec9 | ||
|
|
bdc9aee689 | ||
|
|
2f53ae0ab8 | ||
|
|
f76c05448c | ||
|
|
585e7e8177 | ||
|
|
82d8d1d873 | ||
|
|
2c523c86ff | ||
|
|
6616668d4a | ||
|
|
a4d23d527b | ||
|
|
8a3be85e97 | ||
|
|
1a7f2b7379 | ||
|
|
caac82dffd | ||
|
|
27b48378e5 | ||
|
|
cfd3c3ca5c | ||
|
|
41f26b7a4f | ||
|
|
c57ebb1cea | ||
|
|
259aded5cc | ||
|
|
1f5e091dd7 | ||
|
|
bbb1c44977 | ||
|
|
4327ecbfdf | ||
|
|
114c10bc77 | ||
|
|
cecdf29721 | ||
|
|
bd717fc0d7 | ||
|
|
739fff68b3 | ||
|
|
d49cab0afa | ||
|
|
ffebb4fdaf | ||
|
|
32565f0e53 | ||
|
|
61a5889bea | ||
|
|
82bd3e1ea6 | ||
|
|
b800a6fba2 | ||
|
|
1b6dbfdfff | ||
|
|
061a70f1b3 | ||
|
|
f1c1e0dafd | ||
|
|
c3912fdb37 | ||
|
|
356bfa4860 | ||
|
|
38446668c4 | ||
|
|
e2bb3b8015 | ||
|
|
11dcfd703d | ||
|
|
0b09d23bd1 | ||
|
|
879b726537 | ||
|
|
66bbd03262 | ||
|
|
c09b63698f | ||
|
|
bed7db0a33 | ||
|
|
8ff3e234c1 | ||
|
|
296da5a5c4 | ||
|
|
700a4f1ba1 | ||
|
|
6f6663d825 | ||
|
|
844cdc907f | ||
|
|
9e32d4cb92 | ||
|
|
85dc6ba148 | ||
|
|
16c4d27e00 | ||
|
|
69170fe0e0 | ||
|
|
a646ba54b3 | ||
|
|
45d60fc3c8 | ||
|
|
6f0c399ccd | ||
|
|
9b1d52d183 | ||
|
|
6247fac6c5 | ||
|
|
f27a2c541e | ||
|
|
2317a23fd4 | ||
|
|
26994684d7 | ||
|
|
14a5e036a4 | ||
|
|
5b1c1ca522 | ||
|
|
a9c8458c51 | ||
|
|
5f31d6cbe1 | ||
|
|
477fd8a62d | ||
|
|
90999ee709 | ||
|
|
38065f0cdf | ||
|
|
c77d16cdb3 | ||
|
|
9410084e6a | ||
|
|
e67c5838a9 | ||
|
|
a141cbc7f7 | ||
|
|
6a5352cf3a | ||
|
|
27762637ee | ||
|
|
dcebe34891 | ||
|
|
99d510815f | ||
|
|
928962c08b | ||
|
|
cdadd6db1d | ||
|
|
d45b8ae70b | ||
|
|
445a58c864 | ||
|
|
1895b474ea | ||
|
|
8990ff39a5 | ||
|
|
71e4c97255 | ||
|
|
577e960e28 | ||
|
|
f232f75d40 | ||
|
|
e1a7f829b4 | ||
|
|
f82177b9da | ||
|
|
c1c553bad0 | ||
|
|
588ea02f63 | ||
|
|
ddc69f2268 | ||
|
|
7c62689319 | ||
|
|
df7011167c | ||
|
|
28a785acb0 | ||
|
|
3ee557bfbe | ||
|
|
af94255166 | ||
|
|
4a0dea3f75 | ||
|
|
6a42bc9655 | ||
|
|
c6c8a7f6b7 | ||
|
|
133dd0e26d | ||
|
|
815339272f | ||
|
|
9c1f340029 | ||
|
|
b72ea9608d |
@@ -6,7 +6,6 @@
|
|||||||
"fixed": [
|
"fixed": [
|
||||||
[
|
[
|
||||||
"cojson",
|
"cojson",
|
||||||
"cojson-storage",
|
|
||||||
"cojson-storage-indexeddb",
|
"cojson-storage-indexeddb",
|
||||||
"cojson-storage-sqlite",
|
"cojson-storage-sqlite",
|
||||||
"cojson-transport-ws",
|
"cojson-transport-ws",
|
||||||
|
|||||||
52
.github/workflows/playwright-homepage.yml
vendored
52
.github/workflows/playwright-homepage.yml
vendored
@@ -1,52 +0,0 @@
|
|||||||
name: Playwright Tests - Homepage
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# For pushes, this lets concurrent runs happen, so each push gets a result.
|
|
||||||
# But for other events (e.g. PRs), we can cancel the previous runs.
|
|
||||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
timeout-minutes: 60
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Source Code
|
|
||||||
uses: ./.github/actions/source-code/
|
|
||||||
|
|
||||||
- name: Install root dependencies
|
|
||||||
run: pnpm install && pnpm exec turbo build --filter="./packages/*"
|
|
||||||
|
|
||||||
- name: Install project dependencies
|
|
||||||
run: pnpm install
|
|
||||||
working-directory: ./homepage/homepage
|
|
||||||
|
|
||||||
- name: Pnpm Build
|
|
||||||
run: pnpm exec turbo build
|
|
||||||
working-directory: ./homepage/homepage
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: pnpm exec playwright install
|
|
||||||
working-directory: ./homepage/homepage
|
|
||||||
|
|
||||||
- name: Run Playwright tests
|
|
||||||
run: pnpm exec playwright test
|
|
||||||
working-directory: ./homepage/homepage
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: homepage-playwright-report
|
|
||||||
path: ./homepage/homepage/playwright-report/
|
|
||||||
retention-days: 30
|
|
||||||
157
.github/workflows/playwright.yml
vendored
157
.github/workflows/playwright.yml
vendored
@@ -19,21 +19,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
project: [
|
shard: ["1/2", "2/2"]
|
||||||
"tests/e2e",
|
|
||||||
"examples/chat",
|
|
||||||
"examples/chat-svelte",
|
|
||||||
"examples/clerk",
|
|
||||||
"examples/betterauth",
|
|
||||||
"examples/file-share-svelte",
|
|
||||||
"examples/form",
|
|
||||||
"examples/inspector",
|
|
||||||
"examples/music-player",
|
|
||||||
"examples/organization",
|
|
||||||
"starters/react-passkey-auth",
|
|
||||||
"starters/svelte-passkey-auth",
|
|
||||||
"tests/jazz-svelte"
|
|
||||||
]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -43,25 +29,130 @@ jobs:
|
|||||||
- name: Setup Source Code
|
- name: Setup Source Code
|
||||||
uses: ./.github/actions/source-code/
|
uses: ./.github/actions/source-code/
|
||||||
|
|
||||||
- name: Pnpm Build
|
|
||||||
run: |
|
|
||||||
if [ -f .env.test ]; then
|
|
||||||
cp .env.test .env
|
|
||||||
fi
|
|
||||||
pnpm turbo build
|
|
||||||
working-directory: ./${{ matrix.project }}
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: pnpm exec playwright install
|
run: pnpm exec playwright install
|
||||||
working-directory: ./${{ matrix.project }}
|
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests for shard ${{ matrix.shard }}
|
||||||
run: pnpm exec playwright test
|
run: |
|
||||||
working-directory: ./${{ matrix.project }}
|
# Parse shard information (e.g., "1/2" -> shard_num=1, total_shards=2)
|
||||||
|
IFS='/' read -r shard_num total_shards <<< "${{ matrix.shard }}"
|
||||||
|
shard_index=$((shard_num - 1)) # Convert to 0-based index
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
# Debug: Print parsed values
|
||||||
if: failure()
|
echo "Parsed shard_num: $shard_num"
|
||||||
with:
|
echo "Parsed total_shards: $total_shards"
|
||||||
name: ${{ hashFiles(format('{0}/package.json', matrix.project)) }}-playwright-report
|
echo "Calculated shard_index: $shard_index"
|
||||||
path: ./${{ matrix.project }}/playwright-report/
|
|
||||||
retention-days: 30
|
# Define all projects to test
|
||||||
|
all_projects=(
|
||||||
|
"tests/e2e"
|
||||||
|
"examples/chat"
|
||||||
|
"examples/chat-svelte"
|
||||||
|
"examples/clerk"
|
||||||
|
"examples/betterauth"
|
||||||
|
"examples/file-share-svelte"
|
||||||
|
"examples/form"
|
||||||
|
"examples/inspector"
|
||||||
|
"examples/music-player"
|
||||||
|
"examples/organization"
|
||||||
|
"examples/server-worker-http"
|
||||||
|
"starters/react-passkey-auth"
|
||||||
|
"starters/svelte-passkey-auth"
|
||||||
|
"tests/jazz-svelte"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate which projects this shard should run
|
||||||
|
shard_projects=()
|
||||||
|
for i in "${!all_projects[@]}"; do
|
||||||
|
if [ $((i % total_shards)) -eq $shard_index ]; then
|
||||||
|
shard_projects+=("${all_projects[i]}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Track project results
|
||||||
|
overall_exit_code=0
|
||||||
|
failed_projects=()
|
||||||
|
passed_projects=()
|
||||||
|
|
||||||
|
echo "=== Running tests for shard ${{ matrix.shard }} ==="
|
||||||
|
echo "Projects in this shard:"
|
||||||
|
printf '%s\n' "${shard_projects[@]}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Run tests for each project
|
||||||
|
for project in "${shard_projects[@]}"; do
|
||||||
|
echo "=== Testing project: $project ==="
|
||||||
|
|
||||||
|
# Check if project directory exists
|
||||||
|
if [ ! -d "$project" ]; then
|
||||||
|
echo "❌ FAILED: Project directory $project does not exist"
|
||||||
|
failed_projects+=("$project (directory not found)")
|
||||||
|
overall_exit_code=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if project has package.json
|
||||||
|
if [ ! -f "$project/package.json" ]; then
|
||||||
|
echo "❌ FAILED: No package.json found in $project"
|
||||||
|
failed_projects+=("$project (no package.json)")
|
||||||
|
overall_exit_code=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
echo "🔨 Building $project..."
|
||||||
|
cd "$project"
|
||||||
|
|
||||||
|
if [ -f .env.test ]; then
|
||||||
|
cp .env.test .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! pnpm turbo build; then
|
||||||
|
echo "❌ BUILD FAILED: $project"
|
||||||
|
failed_projects+=("$project (build failed)")
|
||||||
|
overall_exit_code=1
|
||||||
|
cd - > /dev/null
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run Playwright tests
|
||||||
|
echo "🧪 Running Playwright tests for $project..."
|
||||||
|
if ! pnpm exec playwright test; then
|
||||||
|
echo "❌ TESTS FAILED: $project"
|
||||||
|
failed_projects+=("$project (tests failed)")
|
||||||
|
overall_exit_code=1
|
||||||
|
else
|
||||||
|
echo "✅ TESTS PASSED: $project"
|
||||||
|
passed_projects+=("$project")
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd - > /dev/null
|
||||||
|
echo "=== Finished testing $project ==="
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print summary report
|
||||||
|
echo "=========================================="
|
||||||
|
echo "📊 TEST SUMMARY FOR SHARD ${{ matrix.shard }}"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
if [ ${#passed_projects[@]} -gt 0 ]; then
|
||||||
|
echo "✅ PASSED (${#passed_projects[@]}):"
|
||||||
|
printf ' - %s\n' "${passed_projects[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#failed_projects[@]} -gt 0 ]; then
|
||||||
|
echo "❌ FAILED (${#failed_projects[@]}):"
|
||||||
|
printf ' - %s\n' "${failed_projects[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo "Total projects in shard: ${#shard_projects[@]}"
|
||||||
|
echo "Passed: ${#passed_projects[@]}"
|
||||||
|
echo "Failed: ${#failed_projects[@]}"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Exit with overall status
|
||||||
|
exit $overall_exit_code
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"tests/jazz-svelte/src/**",
|
"tests/jazz-svelte/src/**",
|
||||||
"examples/*svelte*/**",
|
"examples/*svelte*/**",
|
||||||
"starters/*svelte*/**",
|
"starters/*svelte*/**",
|
||||||
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
|
"examples/server-worker-inbox/src/routeTree.gen.ts",
|
||||||
"homepage/homepage/**",
|
"homepage/homepage/**",
|
||||||
"**/package.json"
|
"**/package.json"
|
||||||
]
|
]
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": ["packages/cojson-storage*/**", "cojson-transport-ws/**"],
|
"include": ["packages/cojson/src/storage/*/**", "cojson-transport-ws/**"],
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
"@bacons/text-decoder": "^0.0.0",
|
"@bacons/text-decoder": "^0.0.0",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"expo": "~53.0.9",
|
"expo": "54.0.0-canary-20250701-6a945c5",
|
||||||
"expo-clipboard": "^7.1.4",
|
"expo-clipboard": "^7.1.4",
|
||||||
"expo-secure-store": "~14.2.3",
|
"expo-secure-store": "~14.2.3",
|
||||||
"expo-sqlite": "~15.2.10",
|
"expo-sqlite": "~15.2.10",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.79.2",
|
"react-native": "0.80.0",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"readable-stream": "^4.7.0"
|
"readable-stream": "^4.7.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ react {
|
|||||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||||
// root = file("../../")
|
// root = file("../../")
|
||||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||||
// reactNativeDir = file("../../node_modules/react-native")
|
reactNativeDir = file("../../../../node_modules/react-native")
|
||||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
codegenDir = file("../../../../node_modules/@react-native/codegen")
|
||||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||||
// cliFile = file("../../node_modules/react-native/cli.js")
|
cliFile = file("../../../../node_modules/react-native/cli.js")
|
||||||
|
|
||||||
/* Variants */
|
/* Variants */
|
||||||
// The list of variants to that are debuggable. For those we're going to
|
// The list of variants to that are debuggable. For those we're going to
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") }
|
||||||
plugins { id("com.facebook.react.settings") }
|
plugins { id("com.facebook.react.settings") }
|
||||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||||
rootProject.name = 'ChatRN'
|
rootProject.name = 'ChatRN'
|
||||||
include ':app'
|
include ':app'
|
||||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
includeBuild('../../../node_modules/@react-native/gradle-plugin')
|
||||||
|
|||||||
@@ -380,7 +380,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
" ",
|
" ",
|
||||||
);
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
@@ -452,7 +452,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
" ",
|
" ",
|
||||||
);
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
|
|||||||
@@ -2370,87 +2370,87 @@ PODS:
|
|||||||
- Yoga (0.0.0)
|
- Yoga (0.0.0)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||||
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
- fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
||||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
- FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
|
||||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
- fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
- hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||||
- "op-sqlite (from `../../../node_modules/@op-engineering/op-sqlite`)"
|
- "op-sqlite (from `../../../node_modules/@op-engineering/op-sqlite`)"
|
||||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
- RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||||
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
- RCTRequired (from `../../../node_modules/react-native/Libraries/Required`)
|
||||||
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
|
- RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`)
|
||||||
- React (from `../node_modules/react-native/`)
|
- React (from `../../../node_modules/react-native/`)
|
||||||
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
|
- React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`)
|
||||||
- React-Core (from `../node_modules/react-native/`)
|
- React-Core (from `../../../node_modules/react-native/`)
|
||||||
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
|
- React-Core/RCTWebSocket (from `../../../node_modules/react-native/`)
|
||||||
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
|
- React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`)
|
||||||
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
|
- React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`)
|
||||||
- React-debug (from `../node_modules/react-native/ReactCommon/react/debug`)
|
- React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`)
|
||||||
- React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
|
- React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
|
||||||
- React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
|
- React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
|
||||||
- React-Fabric (from `../node_modules/react-native/ReactCommon`)
|
- React-Fabric (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-FabricComponents (from `../node_modules/react-native/ReactCommon`)
|
- React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-FabricImage (from `../node_modules/react-native/ReactCommon`)
|
- React-FabricImage (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`)
|
- React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`)
|
||||||
- React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
|
- React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
|
||||||
- React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`)
|
- React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`)
|
||||||
- React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
|
- React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`)
|
||||||
- React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
|
- React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
|
||||||
- React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
|
- React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
|
||||||
- React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
|
- React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`)
|
||||||
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
- React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`)
|
||||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
- React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`)
|
- React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`)
|
||||||
- React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
|
- React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
|
||||||
- React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
|
- React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
|
||||||
- React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
|
- React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
|
||||||
- React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`)
|
- React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`)
|
||||||
- React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`)
|
- React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`)
|
||||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
|
||||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||||
- react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`)
|
- react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`)
|
||||||
- react-native-mmkv (from `../../../node_modules/react-native-mmkv`)
|
- react-native-mmkv (from `../../../node_modules/react-native-mmkv`)
|
||||||
- "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)"
|
- "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)"
|
||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
||||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
- React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`)
|
||||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||||
- React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`)
|
- React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`)
|
||||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
- React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||||
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
|
- React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`)
|
||||||
- React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`)
|
- React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`)
|
||||||
- React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
|
- React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`)
|
||||||
- React-RCTFabric (from `../node_modules/react-native/React`)
|
- React-RCTFabric (from `../../../node_modules/react-native/React`)
|
||||||
- React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`)
|
- React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`)
|
||||||
- React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
|
- React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`)
|
||||||
- React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
|
- React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`)
|
||||||
- React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`)
|
- React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`)
|
||||||
- React-RCTRuntime (from `../node_modules/react-native/React/Runtime`)
|
- React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`)
|
||||||
- React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
|
- React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`)
|
||||||
- React-RCTText (from `../node_modules/react-native/Libraries/Text`)
|
- React-RCTText (from `../../../node_modules/react-native/Libraries/Text`)
|
||||||
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
|
- React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`)
|
||||||
- React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`)
|
- React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`)
|
||||||
- React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`)
|
- React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`)
|
||||||
- React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`)
|
- React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`)
|
||||||
- React-rncore (from `../node_modules/react-native/ReactCommon`)
|
- React-rncore (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
|
- React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
|
||||||
- React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`)
|
- React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
|
||||||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
- React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||||
- React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`)
|
- React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
|
||||||
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
|
- React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
|
||||||
- React-timing (from `../node_modules/react-native/ReactCommon/react/timing`)
|
- React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`)
|
||||||
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
|
- React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`)
|
||||||
- ReactAppDependencyProvider (from `build/generated/ios`)
|
- ReactAppDependencyProvider (from `build/generated/ios`)
|
||||||
- ReactCodegen (from `build/generated/ios`)
|
- ReactCodegen (from `build/generated/ios`)
|
||||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- "RNCClipboard (from `../../../node_modules/@react-native-clipboard/clipboard`)"
|
- "RNCClipboard (from `../../../node_modules/@react-native-clipboard/clipboard`)"
|
||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../../../node_modules/react-native-screens`)
|
||||||
- SocketRocket (~> 0.7.1)
|
- SocketRocket (~> 0.7.1)
|
||||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -2458,88 +2458,88 @@ SPEC REPOS:
|
|||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
boost:
|
boost:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||||
DoubleConversion:
|
DoubleConversion:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||||
fast_float:
|
fast_float:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
||||||
FBLazyVector:
|
FBLazyVector:
|
||||||
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
:path: "../../../node_modules/react-native/Libraries/FBLazyVector"
|
||||||
fmt:
|
fmt:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/fmt.podspec"
|
||||||
glog:
|
glog:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec"
|
||||||
hermes-engine:
|
hermes-engine:
|
||||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
:podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||||
:tag: hermes-2025-05-06-RNv0.80.0-4eb6132a5bf0450bf4c6c91987675381d7ac8bca
|
:tag: hermes-2025-05-06-RNv0.80.0-4eb6132a5bf0450bf4c6c91987675381d7ac8bca
|
||||||
op-sqlite:
|
op-sqlite:
|
||||||
:path: "../../../node_modules/@op-engineering/op-sqlite"
|
:path: "../../../node_modules/@op-engineering/op-sqlite"
|
||||||
RCT-Folly:
|
RCT-Folly:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||||
RCTDeprecation:
|
RCTDeprecation:
|
||||||
:path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
|
:path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
|
||||||
RCTRequired:
|
RCTRequired:
|
||||||
:path: "../node_modules/react-native/Libraries/Required"
|
:path: "../../../node_modules/react-native/Libraries/Required"
|
||||||
RCTTypeSafety:
|
RCTTypeSafety:
|
||||||
:path: "../node_modules/react-native/Libraries/TypeSafety"
|
:path: "../../../node_modules/react-native/Libraries/TypeSafety"
|
||||||
React:
|
React:
|
||||||
:path: "../node_modules/react-native/"
|
:path: "../../../node_modules/react-native/"
|
||||||
React-callinvoker:
|
React-callinvoker:
|
||||||
:path: "../node_modules/react-native/ReactCommon/callinvoker"
|
:path: "../../../node_modules/react-native/ReactCommon/callinvoker"
|
||||||
React-Core:
|
React-Core:
|
||||||
:path: "../node_modules/react-native/"
|
:path: "../../../node_modules/react-native/"
|
||||||
React-CoreModules:
|
React-CoreModules:
|
||||||
:path: "../node_modules/react-native/React/CoreModules"
|
:path: "../../../node_modules/react-native/React/CoreModules"
|
||||||
React-cxxreact:
|
React-cxxreact:
|
||||||
:path: "../node_modules/react-native/ReactCommon/cxxreact"
|
:path: "../../../node_modules/react-native/ReactCommon/cxxreact"
|
||||||
React-debug:
|
React-debug:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/debug"
|
:path: "../../../node_modules/react-native/ReactCommon/react/debug"
|
||||||
React-defaultsnativemodule:
|
React-defaultsnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
|
||||||
React-domnativemodule:
|
React-domnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom"
|
||||||
React-Fabric:
|
React-Fabric:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
React-FabricComponents:
|
React-FabricComponents:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
React-FabricImage:
|
React-FabricImage:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
React-featureflags:
|
React-featureflags:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/featureflags"
|
:path: "../../../node_modules/react-native/ReactCommon/react/featureflags"
|
||||||
React-featureflagsnativemodule:
|
React-featureflagsnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
|
||||||
React-graphics:
|
React-graphics:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/graphics"
|
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics"
|
||||||
React-hermes:
|
React-hermes:
|
||||||
:path: "../node_modules/react-native/ReactCommon/hermes"
|
:path: "../../../node_modules/react-native/ReactCommon/hermes"
|
||||||
React-idlecallbacksnativemodule:
|
React-idlecallbacksnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
|
||||||
React-ImageManager:
|
React-ImageManager:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
|
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
|
||||||
React-jserrorhandler:
|
React-jserrorhandler:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jserrorhandler"
|
:path: "../../../node_modules/react-native/ReactCommon/jserrorhandler"
|
||||||
React-jsi:
|
React-jsi:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsi"
|
:path: "../../../node_modules/react-native/ReactCommon/jsi"
|
||||||
React-jsiexecutor:
|
React-jsiexecutor:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
|
:path: "../../../node_modules/react-native/ReactCommon/jsiexecutor"
|
||||||
React-jsinspector:
|
React-jsinspector:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern"
|
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern"
|
||||||
React-jsinspectorcdp:
|
React-jsinspectorcdp:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
|
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
|
||||||
React-jsinspectornetwork:
|
React-jsinspectornetwork:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network"
|
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network"
|
||||||
React-jsinspectortracing:
|
React-jsinspectortracing:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
|
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
|
||||||
React-jsitooling:
|
React-jsitooling:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsitooling"
|
:path: "../../../node_modules/react-native/ReactCommon/jsitooling"
|
||||||
React-jsitracing:
|
React-jsitracing:
|
||||||
:path: "../node_modules/react-native/ReactCommon/hermes/executor/"
|
:path: "../../../node_modules/react-native/ReactCommon/hermes/executor/"
|
||||||
React-logger:
|
React-logger:
|
||||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
:path: "../../../node_modules/react-native/ReactCommon/logger"
|
||||||
React-Mapbuffer:
|
React-Mapbuffer:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
React-microtasksnativemodule:
|
React-microtasksnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||||
react-native-get-random-values:
|
react-native-get-random-values:
|
||||||
:path: "../../../node_modules/react-native-get-random-values"
|
:path: "../../../node_modules/react-native-get-random-values"
|
||||||
react-native-mmkv:
|
react-native-mmkv:
|
||||||
@@ -2547,75 +2547,75 @@ EXTERNAL SOURCES:
|
|||||||
react-native-netinfo:
|
react-native-netinfo:
|
||||||
:path: "../../../node_modules/@react-native-community/netinfo"
|
:path: "../../../node_modules/@react-native-community/netinfo"
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
:path: "../node_modules/react-native-safe-area-context"
|
:path: "../../../node_modules/react-native-safe-area-context"
|
||||||
React-NativeModulesApple:
|
React-NativeModulesApple:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||||
React-oscompat:
|
React-oscompat:
|
||||||
:path: "../node_modules/react-native/ReactCommon/oscompat"
|
:path: "../../../node_modules/react-native/ReactCommon/oscompat"
|
||||||
React-perflogger:
|
React-perflogger:
|
||||||
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
|
:path: "../../../node_modules/react-native/ReactCommon/reactperflogger"
|
||||||
React-performancetimeline:
|
React-performancetimeline:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/performance/timeline"
|
:path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline"
|
||||||
React-RCTActionSheet:
|
React-RCTActionSheet:
|
||||||
:path: "../node_modules/react-native/Libraries/ActionSheetIOS"
|
:path: "../../../node_modules/react-native/Libraries/ActionSheetIOS"
|
||||||
React-RCTAnimation:
|
React-RCTAnimation:
|
||||||
:path: "../node_modules/react-native/Libraries/NativeAnimation"
|
:path: "../../../node_modules/react-native/Libraries/NativeAnimation"
|
||||||
React-RCTAppDelegate:
|
React-RCTAppDelegate:
|
||||||
:path: "../node_modules/react-native/Libraries/AppDelegate"
|
:path: "../../../node_modules/react-native/Libraries/AppDelegate"
|
||||||
React-RCTBlob:
|
React-RCTBlob:
|
||||||
:path: "../node_modules/react-native/Libraries/Blob"
|
:path: "../../../node_modules/react-native/Libraries/Blob"
|
||||||
React-RCTFabric:
|
React-RCTFabric:
|
||||||
:path: "../node_modules/react-native/React"
|
:path: "../../../node_modules/react-native/React"
|
||||||
React-RCTFBReactNativeSpec:
|
React-RCTFBReactNativeSpec:
|
||||||
:path: "../node_modules/react-native/React"
|
:path: "../../../node_modules/react-native/React"
|
||||||
React-RCTImage:
|
React-RCTImage:
|
||||||
:path: "../node_modules/react-native/Libraries/Image"
|
:path: "../../../node_modules/react-native/Libraries/Image"
|
||||||
React-RCTLinking:
|
React-RCTLinking:
|
||||||
:path: "../node_modules/react-native/Libraries/LinkingIOS"
|
:path: "../../../node_modules/react-native/Libraries/LinkingIOS"
|
||||||
React-RCTNetwork:
|
React-RCTNetwork:
|
||||||
:path: "../node_modules/react-native/Libraries/Network"
|
:path: "../../../node_modules/react-native/Libraries/Network"
|
||||||
React-RCTRuntime:
|
React-RCTRuntime:
|
||||||
:path: "../node_modules/react-native/React/Runtime"
|
:path: "../../../node_modules/react-native/React/Runtime"
|
||||||
React-RCTSettings:
|
React-RCTSettings:
|
||||||
:path: "../node_modules/react-native/Libraries/Settings"
|
:path: "../../../node_modules/react-native/Libraries/Settings"
|
||||||
React-RCTText:
|
React-RCTText:
|
||||||
:path: "../node_modules/react-native/Libraries/Text"
|
:path: "../../../node_modules/react-native/Libraries/Text"
|
||||||
React-RCTVibration:
|
React-RCTVibration:
|
||||||
:path: "../node_modules/react-native/Libraries/Vibration"
|
:path: "../../../node_modules/react-native/Libraries/Vibration"
|
||||||
React-rendererconsistency:
|
React-rendererconsistency:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/consistency"
|
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency"
|
||||||
React-renderercss:
|
React-renderercss:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/css"
|
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/css"
|
||||||
React-rendererdebug:
|
React-rendererdebug:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/debug"
|
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug"
|
||||||
React-rncore:
|
React-rncore:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
React-RuntimeApple:
|
React-RuntimeApple:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
|
:path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
|
||||||
React-RuntimeCore:
|
React-RuntimeCore:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/runtime"
|
:path: "../../../node_modules/react-native/ReactCommon/react/runtime"
|
||||||
React-runtimeexecutor:
|
React-runtimeexecutor:
|
||||||
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
|
:path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||||
React-RuntimeHermes:
|
React-RuntimeHermes:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/runtime"
|
:path: "../../../node_modules/react-native/ReactCommon/react/runtime"
|
||||||
React-runtimescheduler:
|
React-runtimescheduler:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
|
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
|
||||||
React-timing:
|
React-timing:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/timing"
|
:path: "../../../node_modules/react-native/ReactCommon/react/timing"
|
||||||
React-utils:
|
React-utils:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/utils"
|
:path: "../../../node_modules/react-native/ReactCommon/react/utils"
|
||||||
ReactAppDependencyProvider:
|
ReactAppDependencyProvider:
|
||||||
:path: build/generated/ios
|
:path: build/generated/ios
|
||||||
ReactCodegen:
|
ReactCodegen:
|
||||||
:path: build/generated/ios
|
:path: build/generated/ios
|
||||||
ReactCommon:
|
ReactCommon:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
RNCClipboard:
|
RNCClipboard:
|
||||||
:path: "../../../node_modules/@react-native-clipboard/clipboard"
|
:path: "../../../node_modules/@react-native-clipboard/clipboard"
|
||||||
RNScreens:
|
RNScreens:
|
||||||
:path: "../node_modules/react-native-screens"
|
:path: "../../../node_modules/react-native-screens"
|
||||||
Yoga:
|
Yoga:
|
||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../../../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||||
@@ -2692,7 +2692,7 @@ SPEC CHECKSUMS:
|
|||||||
React-timing: a275a1c2e6112dba17f8f7dd496d439213bbea0d
|
React-timing: a275a1c2e6112dba17f8f7dd496d439213bbea0d
|
||||||
React-utils: 449a6e1fd53886510e284e80bdbb1b1c6db29452
|
React-utils: 449a6e1fd53886510e284e80bdbb1b1c6db29452
|
||||||
ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0
|
ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0
|
||||||
ReactCodegen: 5d41e1df061200130dd326e55cdfdf94b0289c6e
|
ReactCodegen: d82f538f70f00484d418803f74b5a0ea09cc8689
|
||||||
ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147
|
ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147
|
||||||
RNCClipboard: 54ff19965d7c816febbafe5f520c2c3e7b677a49
|
RNCClipboard: 54ff19965d7c816febbafe5f520c2c3e7b677a49
|
||||||
RNScreens: ee2abe7e0c548eed14e92742e81ed991165c56aa
|
RNScreens: ee2abe7e0c548eed14e92742e81ed991165c56aa
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||||
"@bacons/text-decoder": "0.0.0",
|
"@bacons/text-decoder": "0.0.0",
|
||||||
"@op-engineering/op-sqlite": "14.1.0",
|
"@op-engineering/op-sqlite": "14.1.0",
|
||||||
"@react-native-clipboard/clipboard": "1.16.2",
|
"@react-native-clipboard/clipboard": "1.16.3",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"@react-navigation/native": "7.1.14",
|
"@react-navigation/native": "7.1.14",
|
||||||
"@react-navigation/native-stack": "7.3.19",
|
"@react-navigation/native-stack": "7.3.19",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"@react-native/typescript-config": "0.80.0",
|
"@react-native/typescript-config": "0.80.0",
|
||||||
"@rnx-kit/metro-config": "^2.0.1",
|
"@rnx-kit/metro-config": "^2.0.1",
|
||||||
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
|
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
|
||||||
"@types/react": "19.1.0",
|
"@types/react": "^19.1.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"pod-install": "^0.3.5",
|
"pod-install": "^0.3.5",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
|
|||||||
@@ -1,5 +1,131 @@
|
|||||||
# passkey-svelte
|
# passkey-svelte
|
||||||
|
|
||||||
|
## 0.0.111
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3cd1586]
|
||||||
|
- Updated dependencies [33ebbf0]
|
||||||
|
- jazz-tools@0.16.5
|
||||||
|
|
||||||
|
## 0.0.110
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [16764f6]
|
||||||
|
- jazz-tools@0.16.4
|
||||||
|
|
||||||
|
## 0.0.109
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [43d3511]
|
||||||
|
- jazz-tools@0.16.3
|
||||||
|
|
||||||
|
## 0.0.108
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-tools@0.16.2
|
||||||
|
|
||||||
|
## 0.0.107
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [c62abef]
|
||||||
|
- jazz-tools@0.16.1
|
||||||
|
|
||||||
|
## 0.0.106
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2bbb07b: Introduce a cleaner separation between Zod and CoValue schemas:
|
||||||
|
- Zod schemas and CoValue schemas are fully separated. Zod schemas can only be composed with other Zod schemas. CoValue schemas can be composed with either Zod or other CoValue schemas.
|
||||||
|
- `z.optional()` and `z.discriminatedUnion()` no longer work with CoValue schemas. Use `co.optional()` and `co.discriminatedUnion()` instead.
|
||||||
|
- Internal schema access is now simpler. You no longer need to use Zod’s `.def` to access internals. Use properties like `CoMapSchema.shape`, `CoListSchema.element`, and `CoOptionalSchema.innerType` directly.
|
||||||
|
- CoValue schema types are now namespaced under `co.`. Non-namespaced exports have been removed
|
||||||
|
- CoMap schemas no longer incorrectly inherit from Zod. Previously, methods like `.extend()` and `.partial()` appeared available but could cause unexpected behavior. These methods are now disabled. In their place, `.optional()` has been added, and more Zod-like methods will be introduced in future releases.
|
||||||
|
- Upgraded Zod from `3.25.28` to `3.25.76`.
|
||||||
|
- Removed deprecated `withHelpers` method from CoValue schemas
|
||||||
|
- Removed deprecated `createCoValueObservable` function
|
||||||
|
- Updated dependencies [c09dcdf]
|
||||||
|
- Updated dependencies [2bbb07b]
|
||||||
|
- jazz-tools@0.16.0
|
||||||
|
|
||||||
|
## 0.0.105
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9633d01]
|
||||||
|
- Updated dependencies [4beafb7]
|
||||||
|
- jazz-tools@0.15.16
|
||||||
|
|
||||||
|
## 0.0.104
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3fe53a3]
|
||||||
|
- jazz-tools@0.15.15
|
||||||
|
|
||||||
|
## 0.0.103
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a584590]
|
||||||
|
- Updated dependencies [9acccb5]
|
||||||
|
- jazz-tools@0.15.14
|
||||||
|
|
||||||
|
## 0.0.102
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [6c76ff8]
|
||||||
|
- jazz-tools@0.15.13
|
||||||
|
|
||||||
|
## 0.0.101
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [d1c1b0c]
|
||||||
|
- Updated dependencies [cf4ad72]
|
||||||
|
- jazz-tools@0.15.12
|
||||||
|
|
||||||
|
## 0.0.100
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [bdc9aee]
|
||||||
|
- jazz-tools@0.15.11
|
||||||
|
|
||||||
|
## 0.0.99
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9815ec6]
|
||||||
|
- Updated dependencies [b4fdab4]
|
||||||
|
- jazz-tools@0.15.10
|
||||||
|
|
||||||
|
## 0.0.98
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [27b4837]
|
||||||
|
- jazz-tools@0.15.9
|
||||||
|
|
||||||
|
## 0.0.97
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3844666]
|
||||||
|
- jazz-tools@0.15.8
|
||||||
|
|
||||||
|
## 0.0.96
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [c09b636]
|
||||||
|
- jazz-tools@0.15.7
|
||||||
|
|
||||||
## 0.0.95
|
## 0.0.95
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chat-svelte",
|
"name": "chat-svelte",
|
||||||
"version": "0.0.95",
|
"version": "0.0.111",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { co, z } from 'jazz-tools';
|
import { co } from 'jazz-tools';
|
||||||
|
|
||||||
export const Message = co.map({
|
export const Message = co.map({
|
||||||
text: co.plainText(),
|
text: co.plainText(),
|
||||||
image: z.optional(co.image())
|
image: co.optional(co.image())
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Chat = co.list(Message);
|
export const Chat = co.list(Message);
|
||||||
|
|||||||
@@ -16,15 +16,15 @@
|
|||||||
"hash-slash": "workspace:*",
|
"hash-slash": "workspace:*",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.274.0",
|
"lucide-react": "^0.274.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"zod": "3.25.28"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { co, z } from "jazz-tools";
|
import { co } from "jazz-tools";
|
||||||
|
|
||||||
export const Message = co.map({
|
export const Message = co.map({
|
||||||
text: co.plainText(),
|
text: co.plainText(),
|
||||||
image: z.optional(co.image()),
|
image: co.optional(co.image()),
|
||||||
});
|
});
|
||||||
export type Message = co.loaded<typeof Message>;
|
export type Message = co.loaded<typeof Message>;
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||||
"@clerk/clerk-expo": "^2.13.1",
|
"@clerk/clerk-expo": "^2.13.1",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"expo": "~53.0.9",
|
"expo": "54.0.0-canary-20250701-6a945c5",
|
||||||
"expo-crypto": "~14.1.5",
|
"expo-crypto": "~14.1.5",
|
||||||
"expo-linking": "~7.1.5",
|
"expo-linking": "~7.1.5",
|
||||||
"expo-secure-store": "~14.2.3",
|
"expo-secure-store": "~14.2.3",
|
||||||
"expo-sqlite": "~15.2.10",
|
"expo-sqlite": "~15.2.10",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.79.2",
|
"react-native": "0.80.0",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"readable-stream": "^4.7.0"
|
"readable-stream": "^4.7.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,14 +14,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/clerk-react": "^5.4.1",
|
"@clerk/clerk-react": "^5.4.1",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { co, z } from "jazz-tools";
|
import { co } from "jazz-tools";
|
||||||
|
|
||||||
export const JazzProfile = co.profile({
|
export const JazzProfile = co.profile({
|
||||||
file: z.optional(co.fileStream()),
|
file: co.optional(co.fileStream()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const JazzAccount = co.account({
|
export const JazzAccount = co.account({
|
||||||
|
|||||||
@@ -12,16 +12,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hash-slash": "workspace:*",
|
"hash-slash": "workspace:*",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useIframeHashRouter } from "hash-slash";
|
import { useIframeHashRouter } from "hash-slash";
|
||||||
import { Loaded } from "jazz-tools";
|
|
||||||
import { useAccount, useCoState } from "jazz-tools/react";
|
import { useAccount, useCoState } from "jazz-tools/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Errors } from "./Errors.tsx";
|
import { Errors } from "./Errors.tsx";
|
||||||
@@ -9,7 +8,7 @@ import {
|
|||||||
BubbleTeaOrder,
|
BubbleTeaOrder,
|
||||||
DraftBubbleTeaOrder,
|
DraftBubbleTeaOrder,
|
||||||
JazzAccount,
|
JazzAccount,
|
||||||
ListOfBubbleTeaAddOns,
|
validateDraftOrder,
|
||||||
} from "./schema.ts";
|
} from "./schema.ts";
|
||||||
|
|
||||||
export function CreateOrder() {
|
export function CreateOrder() {
|
||||||
@@ -21,20 +20,19 @@ export function CreateOrder() {
|
|||||||
|
|
||||||
if (!me?.root) return;
|
if (!me?.root) return;
|
||||||
|
|
||||||
const onSave = (draft: Loaded<typeof DraftBubbleTeaOrder>) => {
|
const onSave = (draft: DraftBubbleTeaOrder) => {
|
||||||
// validate if the draft is a valid order
|
const validation = validateDraftOrder(draft);
|
||||||
const validation = DraftBubbleTeaOrder.validate(draft);
|
|
||||||
setErrors(validation.errors);
|
setErrors(validation.errors);
|
||||||
if (validation.errors.length > 0) {
|
if (validation.errors.length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// turn the draft into a real order
|
// turn the draft into a real order
|
||||||
me.root.orders.push(draft as Loaded<typeof BubbleTeaOrder>);
|
me.root.orders.push(draft as BubbleTeaOrder);
|
||||||
|
|
||||||
// reset the draft
|
// reset the draft
|
||||||
me.root.draft = DraftBubbleTeaOrder.create({
|
me.root.draft = DraftBubbleTeaOrder.create({
|
||||||
addOns: ListOfBubbleTeaAddOns.create([]),
|
addOns: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.navigate("/");
|
router.navigate("/");
|
||||||
@@ -60,7 +58,7 @@ function CreateOrderForm({
|
|||||||
onSave,
|
onSave,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
onSave: (draft: Loaded<typeof DraftBubbleTeaOrder>) => void;
|
onSave: (draft: DraftBubbleTeaOrder) => void;
|
||||||
}) {
|
}) {
|
||||||
const draft = useCoState(DraftBubbleTeaOrder, id, {
|
const draft = useCoState(DraftBubbleTeaOrder, id, {
|
||||||
resolve: { addOns: true, instructions: true },
|
resolve: { addOns: true, instructions: true },
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useAccount } from "jazz-tools/react";
|
import { useAccount } from "jazz-tools/react";
|
||||||
import { DraftBubbleTeaOrder, JazzAccount } from "./schema";
|
import { JazzAccount, hasChanges } from "./schema";
|
||||||
export function DraftIndicator() {
|
export function DraftIndicator() {
|
||||||
const { me } = useAccount(JazzAccount, {
|
const { me } = useAccount(JazzAccount, {
|
||||||
resolve: { root: { draft: true } },
|
resolve: { root: { draft: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (DraftBubbleTeaOrder.hasChanges(me?.root.draft)) {
|
if (hasChanges(me?.root.draft)) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute -top-1 -right-1 bg-blue-500 border-2 border-white w-3 h-3 rounded-full dark:border-stone-925">
|
<div className="absolute -top-1 -right-1 bg-blue-500 border-2 border-white w-3 h-3 rounded-full dark:border-stone-925">
|
||||||
<span className="sr-only">You have a draft</span>
|
<span className="sr-only">You have a draft</span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CoPlainText, Loaded } from "jazz-tools";
|
import { CoPlainText } from "jazz-tools";
|
||||||
import {
|
import {
|
||||||
BubbleTeaAddOnTypes,
|
BubbleTeaAddOnTypes,
|
||||||
BubbleTeaBaseTeaTypes,
|
BubbleTeaBaseTeaTypes,
|
||||||
@@ -10,7 +10,7 @@ export function OrderForm({
|
|||||||
order,
|
order,
|
||||||
onSave,
|
onSave,
|
||||||
}: {
|
}: {
|
||||||
order: Loaded<typeof BubbleTeaOrder> | Loaded<typeof DraftBubbleTeaOrder>;
|
order: BubbleTeaOrder | DraftBubbleTeaOrder;
|
||||||
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
|
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
}) {
|
}) {
|
||||||
// Handles updates to the instructions field of the order.
|
// Handles updates to the instructions field of the order.
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Loaded } from "jazz-tools";
|
|
||||||
import { BubbleTeaOrder } from "./schema.ts";
|
import { BubbleTeaOrder } from "./schema.ts";
|
||||||
|
|
||||||
export function OrderThumbnail({
|
export function OrderThumbnail({
|
||||||
order,
|
order,
|
||||||
}: {
|
}: {
|
||||||
order: Loaded<typeof BubbleTeaOrder>;
|
order: BubbleTeaOrder;
|
||||||
}) {
|
}) {
|
||||||
const { id, baseTea, addOns, instructions, deliveryDate, withMilk } = order;
|
const { id, baseTea, addOns, instructions, deliveryDate, withMilk } = order;
|
||||||
const date = deliveryDate.toLocaleDateString();
|
const date = deliveryDate.toLocaleDateString();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Loaded, co, z } from "jazz-tools";
|
import { co, z } from "jazz-tools";
|
||||||
|
|
||||||
export const BubbleTeaAddOnTypes = [
|
export const BubbleTeaAddOnTypes = [
|
||||||
"Pearl",
|
"Pearl",
|
||||||
@@ -15,52 +15,46 @@ export const BubbleTeaBaseTeaTypes = [
|
|||||||
"Thai",
|
"Thai",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const ListOfBubbleTeaAddOns = co
|
export const ListOfBubbleTeaAddOns = co.list(
|
||||||
.list(z.literal([...BubbleTeaAddOnTypes]))
|
z.literal([...BubbleTeaAddOnTypes]),
|
||||||
.withHelpers((Self) => ({
|
);
|
||||||
hasChanges(list?: Loaded<typeof Self> | null) {
|
export type ListOfBubbleTeaAddOns = co.loaded<typeof ListOfBubbleTeaAddOns>;
|
||||||
return list && Object.entries(list._raw.insertions).length > 0;
|
|
||||||
},
|
function hasAddOnsChanges(list?: ListOfBubbleTeaAddOns | null) {
|
||||||
}));
|
return list && Object.entries(list._raw.insertions).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export const BubbleTeaOrder = co.map({
|
export const BubbleTeaOrder = co.map({
|
||||||
baseTea: z.literal([...BubbleTeaBaseTeaTypes]),
|
baseTea: z.literal([...BubbleTeaBaseTeaTypes]),
|
||||||
addOns: ListOfBubbleTeaAddOns,
|
addOns: ListOfBubbleTeaAddOns,
|
||||||
deliveryDate: z.date(),
|
deliveryDate: z.date(),
|
||||||
withMilk: z.boolean(),
|
withMilk: z.boolean(),
|
||||||
instructions: z.optional(co.plainText()),
|
instructions: co.optional(co.plainText()),
|
||||||
});
|
});
|
||||||
|
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
|
||||||
|
|
||||||
export const DraftBubbleTeaOrder = co
|
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
|
||||||
.map({
|
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
|
||||||
baseTea: z.optional(z.literal([...BubbleTeaBaseTeaTypes])),
|
|
||||||
addOns: z.optional(ListOfBubbleTeaAddOns),
|
|
||||||
deliveryDate: z.optional(z.date()),
|
|
||||||
withMilk: z.optional(z.boolean()),
|
|
||||||
instructions: z.optional(co.plainText()),
|
|
||||||
})
|
|
||||||
.withHelpers((Self) => ({
|
|
||||||
hasChanges(order: Loaded<typeof Self> | undefined) {
|
|
||||||
return (
|
|
||||||
!!order &&
|
|
||||||
(Object.keys(order._edits).length > 1 ||
|
|
||||||
ListOfBubbleTeaAddOns.hasChanges(order.addOns))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
validate(order: Loaded<typeof Self>) {
|
export function validateDraftOrder(order: DraftBubbleTeaOrder) {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!order.baseTea) {
|
if (!order.baseTea) {
|
||||||
errors.push("Please select your preferred base tea.");
|
errors.push("Please select your preferred base tea.");
|
||||||
}
|
}
|
||||||
if (!order.deliveryDate) {
|
if (!order.deliveryDate) {
|
||||||
errors.push("Plese select a delivery date.");
|
errors.push("Plese select a delivery date.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { errors };
|
return { errors };
|
||||||
},
|
}
|
||||||
}));
|
|
||||||
|
export function hasChanges(order?: DraftBubbleTeaOrder | null) {
|
||||||
|
return (
|
||||||
|
!!order &&
|
||||||
|
(Object.keys(order._edits).length > 1 || hasAddOnsChanges(order.addOns))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** The root is an app-specific per-user private `CoMap`
|
/** The root is an app-specific per-user private `CoMap`
|
||||||
* where you can store top-level objects for that user */
|
* where you can store top-level objects for that user */
|
||||||
@@ -76,15 +70,9 @@ export const JazzAccount = co
|
|||||||
})
|
})
|
||||||
.withMigration((account) => {
|
.withMigration((account) => {
|
||||||
if (!account.root) {
|
if (!account.root) {
|
||||||
const orders = co.list(BubbleTeaOrder).create([], account);
|
account.root = AccountRoot.create(
|
||||||
const draft = DraftBubbleTeaOrder.create(
|
{ draft: { addOns: [], instructions: "" }, orders: [] },
|
||||||
{
|
|
||||||
addOns: ListOfBubbleTeaAddOns.create([], account),
|
|
||||||
instructions: co.plainText().create("", account),
|
|
||||||
},
|
|
||||||
account,
|
account,
|
||||||
);
|
);
|
||||||
|
|
||||||
account.root = AccountRoot.create({ draft, orders }, account);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { co, z } from "jazz-tools";
|
import { co } from "jazz-tools";
|
||||||
|
|
||||||
export const JazzProfile = co.profile({
|
export const JazzProfile = co.profile({
|
||||||
image: z.optional(co.image()),
|
image: co.optional(co.image()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const JazzAccount = co.account({
|
export const JazzAccount = co.account({
|
||||||
|
|||||||
@@ -17,15 +17,15 @@
|
|||||||
"cojson-transport-ws": "workspace:*",
|
"cojson-transport-ws": "workspace:*",
|
||||||
"hash-slash": "workspace:*",
|
"hash-slash": "workspace:*",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"react-use": "^17.4.0"
|
"react-use": "^17.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export const Issue = co.map({
|
|||||||
status: z.enum(["open", "closed"]),
|
status: z.enum(["open", "closed"]),
|
||||||
labels: co.list(z.string()),
|
labels: co.list(z.string()),
|
||||||
reactions: ReactionsList,
|
reactions: ReactionsList,
|
||||||
file: z.optional(co.fileStream()),
|
file: co.optional(co.fileStream()),
|
||||||
image: z.optional(co.image()),
|
image: co.optional(co.image()),
|
||||||
lead: z.optional(co.account()),
|
lead: co.optional(co.account()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Project = co.map({
|
export const Project = co.map({
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
|||||||
1
examples/jazz-nextjs/src/apiKey.ts
Normal file
1
examples/jazz-nextjs/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const apiKey = "jazz-nextjs@garden.co";
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { JazzInspector } from "jazz-tools/inspector";
|
import { JazzInspector } from "jazz-tools/inspector";
|
||||||
import { JazzReactProvider } from "jazz-tools/react";
|
import { JazzReactProvider } from "jazz-tools/react";
|
||||||
|
import { apiKey } from "./apiKey";
|
||||||
|
|
||||||
export function Jazz({ children }: { children: React.ReactNode }) {
|
export function Jazz({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<JazzReactProvider
|
<JazzReactProvider
|
||||||
enableSSR
|
enableSSR
|
||||||
sync={{
|
sync={{
|
||||||
peer: `wss://cloud.jazz.tools/`,
|
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/web": "^9.7.5",
|
"@react-spring/web": "^9.7.5",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"zod": "3.25.28"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/clerk-react": "^5.4.1",
|
"@clerk/clerk-react": "^5.4.1",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.274.0",
|
"lucide-react": "^0.274.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"react-router": "^6.16.0",
|
"react-router": "^6.16.0",
|
||||||
"react-router-dom": "^6.16.0",
|
"react-router-dom": "^6.16.0",
|
||||||
"tailwind-merge": "^1.14.0"
|
"tailwind-merge": "^1.14.0"
|
||||||
@@ -32,12 +32,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-pwa": "^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const MusicaAccountRoot = co.map({
|
|||||||
// track and playlist
|
// track and playlist
|
||||||
// You can also add the position in time if you want make it possible
|
// You can also add the position in time if you want make it possible
|
||||||
// to resume the song
|
// to resume the song
|
||||||
activeTrack: z.optional(MusicTrack),
|
activeTrack: co.optional(MusicTrack),
|
||||||
activePlaylist: Playlist,
|
activePlaylist: Playlist,
|
||||||
|
|
||||||
exampleDataLoaded: z.optional(z.boolean()),
|
exampleDataLoaded: z.optional(z.boolean()),
|
||||||
@@ -84,15 +84,14 @@ export const MusicaAccount = co
|
|||||||
* You can use it to set up the account root and any other initial CoValues you need.
|
* You can use it to set up the account root and any other initial CoValues you need.
|
||||||
*/
|
*/
|
||||||
if (account.root === undefined) {
|
if (account.root === undefined) {
|
||||||
const tracks = co.list(MusicTrack).create([]);
|
|
||||||
const rootPlaylist = Playlist.create({
|
const rootPlaylist = Playlist.create({
|
||||||
tracks,
|
tracks: [],
|
||||||
title: "",
|
title: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
account.root = MusicaAccountRoot.create({
|
account.root = MusicaAccountRoot.create({
|
||||||
rootPlaylist,
|
rootPlaylist,
|
||||||
playlists: co.list(Playlist).create([]),
|
playlists: [],
|
||||||
activeTrack: undefined,
|
activeTrack: undefined,
|
||||||
activePlaylist: rootPlaylist,
|
activePlaylist: rootPlaylist,
|
||||||
exampleDataLoaded: false,
|
exampleDataLoaded: false,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { uploadMusicTracks } from "./4_actions";
|
|||||||
import { MediaPlayer } from "./5_useMediaPlayer";
|
import { MediaPlayer } from "./5_useMediaPlayer";
|
||||||
import { FileUploadButton } from "./components/FileUploadButton";
|
import { FileUploadButton } from "./components/FileUploadButton";
|
||||||
import { MusicTrackRow } from "./components/MusicTrackRow";
|
import { MusicTrackRow } from "./components/MusicTrackRow";
|
||||||
|
import { PlayerControls } from "./components/PlayerControls";
|
||||||
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
|
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
|
||||||
import { SidePanel } from "./components/SidePanel";
|
import { SidePanel } from "./components/SidePanel";
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./components/ui/button";
|
||||||
@@ -42,7 +43,11 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
|
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
|
||||||
|
|
||||||
const playlist = useCoState(Playlist, playlistId, {
|
const playlist = useCoState(Playlist, playlistId, {
|
||||||
resolve: { tracks: true },
|
resolve: {
|
||||||
|
tracks: {
|
||||||
|
$each: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isRootPlaylist = !params.playlistId;
|
const isRootPlaylist = !params.playlistId;
|
||||||
@@ -66,8 +71,8 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
return (
|
return (
|
||||||
<SidebarInset className="flex flex-col h-screen text-gray-800 bg-blue-50">
|
<SidebarInset className="flex flex-col h-screen text-gray-800 bg-blue-50">
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<SidePanel mediaPlayer={mediaPlayer} />
|
<SidePanel />
|
||||||
<main className="flex-1 p-6 overflow-y-auto overflow-x-hidden">
|
<main className="flex-1 p-6 overflow-y-auto overflow-x-hidden relative">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
{isRootPlaylist ? (
|
{isRootPlaylist ? (
|
||||||
@@ -106,12 +111,12 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
mediaPlayer.setActiveTrack(track, playlist);
|
mediaPlayer.setActiveTrack(track, playlist);
|
||||||
}}
|
}}
|
||||||
showAddToPlaylist={isRootPlaylist}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
|
<PlayerControls mediaPlayer={mediaPlayer} />
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { getAudioFileData } from "@/lib/audio/getAudioFileData";
|
import { getAudioFileData } from "@/lib/audio/getAudioFileData";
|
||||||
import { FileStream, Group, co } from "jazz-tools";
|
import { FileStream, Group } from "jazz-tools";
|
||||||
import {
|
import { MusicTrack, MusicaAccount, Playlist } from "./1_schema";
|
||||||
MusicTrack,
|
|
||||||
MusicTrackWaveform,
|
|
||||||
MusicaAccount,
|
|
||||||
Playlist,
|
|
||||||
} from "./1_schema";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walkthrough: Actions
|
* Walkthrough: Actions
|
||||||
@@ -51,7 +46,7 @@ export async function uploadMusicTracks(
|
|||||||
{
|
{
|
||||||
file: fileStream,
|
file: fileStream,
|
||||||
duration: data.duration,
|
duration: data.duration,
|
||||||
waveform: MusicTrackWaveform.create({ data: data.waveform }, group),
|
waveform: { data: data.waveform },
|
||||||
title: file.name,
|
title: file.name,
|
||||||
isExampleTrack,
|
isExampleTrack,
|
||||||
},
|
},
|
||||||
@@ -73,18 +68,10 @@ export async function createNewPlaylist() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since playlists are meant to be shared we associate them
|
const playlist = Playlist.create({
|
||||||
// to a group which will contain the keys required to get
|
title: "New Playlist",
|
||||||
// access to the "owned" values
|
tracks: [],
|
||||||
const playlistGroup = Group.create();
|
});
|
||||||
|
|
||||||
const playlist = Playlist.create(
|
|
||||||
{
|
|
||||||
title: "New Playlist",
|
|
||||||
tracks: co.list(MusicTrack).create([], playlistGroup),
|
|
||||||
},
|
|
||||||
playlistGroup,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Again, we associate the new playlist to the
|
// Again, we associate the new playlist to the
|
||||||
// user by pushing it into the playlists CoList
|
// user by pushing it into the playlists CoList
|
||||||
@@ -129,7 +116,7 @@ export async function removeTrackFromPlaylist(
|
|||||||
|
|
||||||
if (track._owner._type === "Group" && playlist._owner._type === "Group") {
|
if (track._owner._type === "Group" && playlist._owner._type === "Group") {
|
||||||
const trackGroup = track._owner;
|
const trackGroup = track._owner;
|
||||||
await trackGroup.removeMember(playlist._owner);
|
trackGroup.removeMember(playlist._owner);
|
||||||
|
|
||||||
const index =
|
const index =
|
||||||
playlist.tracks?.findIndex(
|
playlist.tracks?.findIndex(
|
||||||
|
|||||||
59
examples/music-player/src/components/ConfirmDialog.tsx
Normal file
59
examples/music-player/src/components/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface ConfirmDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmDialog({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
confirmText = "Confirm",
|
||||||
|
cancelText = "Cancel",
|
||||||
|
onConfirm,
|
||||||
|
variant = "destructive",
|
||||||
|
}: ConfirmDialogProps) {
|
||||||
|
function handleConfirm() {
|
||||||
|
onConfirm();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
<Button variant={variant} onClick={handleConfirm}>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,27 +10,34 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Loaded } from "jazz-tools";
|
import { Loaded } from "jazz-tools";
|
||||||
import { useAccount, useCoState } from "jazz-tools/react";
|
import { useAccount, useCoState } from "jazz-tools/react";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
import { Fragment } from "react/jsx-runtime";
|
import { Fragment, useCallback, useState } from "react";
|
||||||
import { MusicTrackTitleInput } from "./MusicTrackTitleInput";
|
import { EditTrackDialog } from "./RenameTrackDialog";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
|
function isPartOfThePlaylist(
|
||||||
|
trackId: string,
|
||||||
|
playlist: Loaded<typeof Playlist, { tracks: true }>,
|
||||||
|
) {
|
||||||
|
return Array.from(playlist.tracks._refs).some((t) => t.id === trackId);
|
||||||
|
}
|
||||||
|
|
||||||
export function MusicTrackRow({
|
export function MusicTrackRow({
|
||||||
trackId,
|
trackId,
|
||||||
isLoading,
|
isLoading,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
onClick,
|
onClick,
|
||||||
showAddToPlaylist,
|
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
onClick: (track: Loaded<typeof MusicTrack>) => void;
|
onClick: (track: Loaded<typeof MusicTrack>) => void;
|
||||||
showAddToPlaylist: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const track = useCoState(MusicTrack, trackId);
|
const track = useCoState(MusicTrack, trackId);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
const { me } = useAccount(MusicaAccount, {
|
const { me } = useAccount(MusicaAccount, {
|
||||||
resolve: { root: { playlists: { $each: true } } },
|
resolve: { root: { playlists: { $each: { tracks: true } } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const playlists = me?.root.playlists ?? [];
|
const playlists = me?.root.playlists ?? [];
|
||||||
@@ -60,10 +67,18 @@ export function MusicTrackRow({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDropdownOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={"flex gap-1 hover:bg-slate-200 group py-2 px-2 cursor-pointer"}
|
className={"flex gap-1 hover:bg-slate-200 group py-2 px-2 cursor-pointer"}
|
||||||
onClick={handleTrackClick}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -81,50 +96,57 @@ export function MusicTrackRow({
|
|||||||
"▶️"
|
"▶️"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<MusicTrackTitleInput trackId={trackId} />
|
<button
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onClick={handleTrackClick}
|
||||||
|
className="w-full flex items-center overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{track?.title}
|
||||||
|
</button>
|
||||||
<div onClick={(evt) => evt.stopPropagation()}>
|
<div onClick={(evt) => evt.stopPropagation()}>
|
||||||
{showAddToPlaylist && (
|
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
className="h-8 w-8 p-0"
|
||||||
className="h-8 w-8 p-0"
|
aria-label={`Open ${track?.title} menu`}
|
||||||
aria-label={`Open ${track?.title} menu`}
|
>
|
||||||
>
|
<span className="sr-only">Open menu</span>
|
||||||
<span className="sr-only">Open menu</span>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuItem onSelect={handleEdit}>Edit</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
{playlists.map((playlist, index) => (
|
||||||
key={`delete`}
|
<Fragment key={index}>
|
||||||
onSelect={async () => {
|
{isPartOfThePlaylist(trackId, playlist) ? (
|
||||||
if (!track) return;
|
|
||||||
deleteTrack();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{playlists.map((playlist, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={`add-${index}`}
|
|
||||||
onSelect={() => handleAddToPlaylist(playlist)}
|
|
||||||
>
|
|
||||||
Add to {playlist.title}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={`remove-${index}`}
|
key={`remove-${index}`}
|
||||||
onSelect={() => handleRemoveFromPlaylist(playlist)}
|
onSelect={() => handleRemoveFromPlaylist(playlist)}
|
||||||
>
|
>
|
||||||
Remove from {playlist.title}
|
Remove from {playlist.title}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Fragment>
|
) : (
|
||||||
))}
|
<DropdownMenuItem
|
||||||
</DropdownMenuContent>
|
key={`add-${index}`}
|
||||||
</DropdownMenu>
|
onSelect={() => handleAddToPlaylist(playlist)}
|
||||||
)}
|
>
|
||||||
|
Add to {playlist.title}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
{track && isEditDialogOpen && (
|
||||||
|
<EditTrackDialog
|
||||||
|
track={track}
|
||||||
|
isOpen={isEditDialogOpen}
|
||||||
|
onOpenChange={setIsEditDialogOpen}
|
||||||
|
onDelete={deleteTrack}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,25 +24,25 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
const activeTrackTitle = activeTrack.title;
|
const activeTrackTitle = activeTrack.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="flex items-center justify-between p-4 gap-4 bg-white border-t border-gray-200 fixed bottom-0 left-0 right-0 w-full">
|
<footer className="flex items-center justify-between p-2 sm:p-4 gap-2 sm:gap-4 bg-white border-t border-gray-200 absolute bottom-0 left-0 right-0 w-full z-50">
|
||||||
<div className="flex justify-center items-center space-x-2">
|
<div className="flex justify-center items-center space-x-1 sm:space-x-2 flex-shrink-0">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-2 sm:space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={mediaPlayer.playPrevTrack}
|
onClick={mediaPlayer.playPrevTrack}
|
||||||
className="text-blue-600 hover:text-blue-800"
|
className="text-blue-600 hover:text-blue-800"
|
||||||
aria-label="Previous track"
|
aria-label="Previous track"
|
||||||
>
|
>
|
||||||
<SkipBack size={20} />
|
<SkipBack size={16} className="sm:w-5 sm:h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={playState.toggle}
|
onClick={playState.toggle}
|
||||||
className="w-[42px] h-[42px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700"
|
className="w-8 h-8 sm:w-[42px] sm:h-[42px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700"
|
||||||
aria-label={isPlaying ? "Pause active track" : "Play active track"}
|
aria-label={isPlaying ? "Pause active track" : "Play active track"}
|
||||||
>
|
>
|
||||||
{isPlaying ? (
|
{isPlaying ? (
|
||||||
<Pause size={24} fill="currentColor" />
|
<Pause size={16} className="sm:w-6 sm:h-6" fill="currentColor" />
|
||||||
) : (
|
) : (
|
||||||
<Play size={24} fill="currentColor" />
|
<Play size={16} className="sm:w-6 sm:h-6" fill="currentColor" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -50,16 +50,22 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
className="text-blue-600 hover:text-blue-800"
|
className="text-blue-600 hover:text-blue-800"
|
||||||
aria-label="Next track"
|
aria-label="Next track"
|
||||||
>
|
>
|
||||||
<SkipForward size={20} />
|
<SkipForward size={16} className="sm:w-5 sm:h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className=" sm:hidden md:flex flex-col flex-shrink-1 items-center w-[75%]">
|
<div className="md:hidden sm:hidden lg:flex flex-1 justify-center items-center min-w-0 px-2">
|
||||||
<Waveform track={activeTrack} height={30} />
|
<Waveform
|
||||||
|
track={activeTrack}
|
||||||
|
height={30}
|
||||||
|
className="h-5 sm:h-6 md:h-8 lg:h-10"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-1 text-right min-w-fit w-[25%]">
|
<div className="flex flex-col items-end gap-1 text-right min-w-fit flex-shrink-0">
|
||||||
<h4 className="font-medium text-blue-800">{activeTrackTitle}</h4>
|
<h4 className="font-medium text-blue-800 text-sm sm:text-base truncate max-w-32 sm:max-w-80">
|
||||||
<p className="text-sm text-gray-600">
|
{activeTrackTitle}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600 truncate max-w-32 sm:max-w-80">
|
||||||
{activePlaylist?.title || "All tracks"}
|
{activePlaylist?.title || "All tracks"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
108
examples/music-player/src/components/RenameTrackDialog.tsx
Normal file
108
examples/music-player/src/components/RenameTrackDialog.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { MusicTrack } from "@/1_schema";
|
||||||
|
import { updateMusicTrackTitle } from "@/4_actions";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Loaded } from "jazz-tools";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ConfirmDialog } from "./ConfirmDialog";
|
||||||
|
|
||||||
|
interface EditTrackDialogProps {
|
||||||
|
track: Loaded<typeof MusicTrack>;
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditTrackDialog({
|
||||||
|
track,
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
onDelete,
|
||||||
|
}: EditTrackDialogProps) {
|
||||||
|
const [newTitle, setNewTitle] = useState(track.title);
|
||||||
|
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (track && newTitle.trim()) {
|
||||||
|
updateMusicTrackTitle(track, newTitle.trim());
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
setNewTitle(track?.title || "");
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteClick() {
|
||||||
|
setIsDeleteConfirmOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteConfirm() {
|
||||||
|
onDelete();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
handleSave();
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Track</DialogTitle>
|
||||||
|
<DialogDescription>Edit "{track?.title}".</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form className="py-4" onSubmit={handleSave}>
|
||||||
|
<Input
|
||||||
|
value={newTitle}
|
||||||
|
onChange={(e) => setNewTitle(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter track name..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<DialogFooter className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
className="mr-auto"
|
||||||
|
>
|
||||||
|
Delete Track
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={!newTitle.trim()}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isDeleteConfirmOpen}
|
||||||
|
onOpenChange={setIsDeleteConfirmOpen}
|
||||||
|
title="Delete Track"
|
||||||
|
description={`Are you sure you want to delete "${track.title}"? This action cannot be undone.`}
|
||||||
|
confirmText="Delete"
|
||||||
|
cancelText="Cancel"
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
|
variant="destructive"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { MusicTrack, MusicaAccount } from "@/1_schema";
|
import { MusicaAccount } from "@/1_schema";
|
||||||
import { createNewPlaylist, deletePlaylist } from "@/4_actions";
|
import { createNewPlaylist, deletePlaylist } from "@/4_actions";
|
||||||
import { MediaPlayer } from "@/5_useMediaPlayer";
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
@@ -14,22 +12,18 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { usePlayState } from "@/lib/audio/usePlayState";
|
import { useAccount } from "jazz-tools/react";
|
||||||
import { useAccount, useCoState } from "jazz-tools/react";
|
import { Home, Music, Plus, Trash2 } from "lucide-react";
|
||||||
import { Home, Music, Pause, Play, Plus, Trash2 } from "lucide-react";
|
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { AuthButton } from "./AuthButton";
|
import { AuthButton } from "./AuthButton";
|
||||||
|
|
||||||
export function SidePanel({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
export function SidePanel() {
|
||||||
const { playlistId } = useParams();
|
const { playlistId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { me } = useAccount(MusicaAccount, {
|
const { me } = useAccount(MusicaAccount, {
|
||||||
resolve: { root: { playlists: { $each: true } } },
|
resolve: { root: { playlists: { $each: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const playState = usePlayState();
|
|
||||||
const isPlaying = playState.value === "play";
|
|
||||||
|
|
||||||
function handleAllTracksClick() {
|
function handleAllTracksClick() {
|
||||||
navigate(`/`);
|
navigate(`/`);
|
||||||
}
|
}
|
||||||
@@ -50,12 +44,6 @@ export function SidePanel({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
navigate(`/playlist/${playlist.id}`);
|
navigate(`/playlist/${playlist.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
|
||||||
resolve: { waveform: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeTrackTitle = activeTrack?.title;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@@ -137,29 +125,6 @@ export function SidePanel({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
{activeTrack && (
|
|
||||||
<SidebarFooter>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem className="flex justify-end">
|
|
||||||
<SidebarMenuButton
|
|
||||||
onClick={playState.toggle}
|
|
||||||
aria-label={
|
|
||||||
isPlaying ? "Pause active track" : "Play active track"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="w-[28px] h-[28px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700">
|
|
||||||
{isPlaying ? (
|
|
||||||
<Pause size={16} fill="currentColor" />
|
|
||||||
) : (
|
|
||||||
<Play size={16} fill="currentColor" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>{activeTrackTitle}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarFooter>
|
|
||||||
)}
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useCoState } from "jazz-tools/react";
|
|||||||
export function Waveform(props: {
|
export function Waveform(props: {
|
||||||
track: Loaded<typeof MusicTrack>;
|
track: Loaded<typeof MusicTrack>;
|
||||||
height: number;
|
height: number;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { track, height } = props;
|
const { track, height } = props;
|
||||||
const waveformData = useCoState(
|
const waveformData = useCoState(
|
||||||
@@ -36,7 +37,7 @@ export function Waveform(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex justify-center items-end w-full"
|
className={cn("flex justify-center items-end w-full", props.className)}
|
||||||
style={{
|
style={{
|
||||||
height,
|
height,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 1200px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: hsl(0 0% 100%);
|
--background: hsl(0 0% 100%);
|
||||||
--foreground: hsl(20 14.3% 4.1%);
|
--foreground: hsl(20 14.3% 4.1%);
|
||||||
|
|||||||
@@ -55,10 +55,20 @@ export class HomePage {
|
|||||||
|
|
||||||
async editTrackTitle(trackTitle: string, newTitle: string) {
|
async editTrackTitle(trackTitle: string, newTitle: string) {
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole("textbox", {
|
.getByRole("button", {
|
||||||
name: `Edit track title: ${trackTitle}`,
|
name: `Open ${trackTitle} menu`,
|
||||||
})
|
})
|
||||||
.fill(newTitle);
|
.click();
|
||||||
|
|
||||||
|
await this.page
|
||||||
|
.getByRole("menuitem", {
|
||||||
|
name: `Edit`,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await this.page.getByPlaceholder("Enter track name...").fill(newTitle);
|
||||||
|
|
||||||
|
await this.page.getByRole("button", { name: "Save" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPlaylist() {
|
async createPlaylist() {
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
strategies: "generateSW",
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
|
||||||
|
maximumFileSizeToCacheInBytes: 1024 * 1024 * 5,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.274.0",
|
"lucide-react": "^0.274.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"react-router": "^6.16.0",
|
"react-router": "^6.16.0",
|
||||||
"react-router-dom": "^6.16.0"
|
"react-router-dom": "^6.16.0"
|
||||||
},
|
},
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
JazzAccount,
|
JazzAccount,
|
||||||
Organization,
|
Organization,
|
||||||
Project,
|
Project,
|
||||||
|
validateDraftOrganization,
|
||||||
} from "../schema.ts";
|
} from "../schema.ts";
|
||||||
import { Errors } from "./Errors.tsx";
|
import { Errors } from "./Errors.tsx";
|
||||||
import { OrganizationForm } from "./OrganizationForm.tsx";
|
import { OrganizationForm } from "./OrganizationForm.tsx";
|
||||||
@@ -21,8 +22,7 @@ export function CreateOrganization() {
|
|||||||
if (!me?.root?.organizations) return;
|
if (!me?.root?.organizations) return;
|
||||||
|
|
||||||
const onSave = (draft: Loaded<typeof DraftOrganization>) => {
|
const onSave = (draft: Loaded<typeof DraftOrganization>) => {
|
||||||
// validate if the draft is a valid organization
|
const validation = validateDraftOrganization(draft);
|
||||||
const validation = DraftOrganization.validate(draft);
|
|
||||||
setErrors(validation.errors);
|
setErrors(validation.errors);
|
||||||
if (validation.errors.length > 0) {
|
if (validation.errors.length > 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -10,24 +10,24 @@ export const Organization = co.map({
|
|||||||
projects: co.list(Project),
|
projects: co.list(Project),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DraftOrganization = co
|
export const DraftOrganization = co.map({
|
||||||
.map({
|
name: z.optional(z.string()),
|
||||||
name: z.optional(z.string()),
|
projects: co.list(Project),
|
||||||
projects: co.list(Project),
|
});
|
||||||
})
|
|
||||||
.withHelpers((Self) => ({
|
|
||||||
validate(org: Loaded<typeof Self>) {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (!org.name) {
|
export function validateDraftOrganization(
|
||||||
errors.push("Please enter a name.");
|
org: Loaded<typeof DraftOrganization>,
|
||||||
}
|
) {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
return {
|
if (!org.name) {
|
||||||
errors,
|
errors.push("Please enter a name.");
|
||||||
};
|
}
|
||||||
},
|
|
||||||
}));
|
return {
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const JazzAccountRoot = co.map({
|
export const JazzAccountRoot = co.map({
|
||||||
organizations: co.list(Organization),
|
organizations: co.list(Organization),
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hash-slash": "workspace:*",
|
"hash-slash": "workspace:*",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
|
|||||||
@@ -19,15 +19,15 @@
|
|||||||
"prosemirror-schema-list": "^1.5.1",
|
"prosemirror-schema-list": "^1.5.1",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-view": "^1.39.1",
|
"prosemirror-view": "^1.39.1",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.509.0",
|
"lucide-react": "^0.509.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
|
|||||||
47
examples/server-worker-http/.gitignore
vendored
Normal file
47
examples/server-worker-http/.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
105
examples/server-worker-http/README.md
Normal file
105
examples/server-worker-http/README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Rock Paper Scissors with Jazz HTTP API
|
||||||
|
|
||||||
|
This example demonstrates how to use the **Jazz HTTP API** with **Next.js** to implement updates in a trusted environment. The application implements a multiplayer Rock Paper Scissors game where the server validates game actions and reveals player intentions only after both players have made their moves.
|
||||||
|
|
||||||
|
## 🎯 Key Concepts Demonstrated
|
||||||
|
|
||||||
|
### Trusted Environment Updates
|
||||||
|
- **Server-side validation**: All game actions are validated by the server before being applied
|
||||||
|
- **Secure reveal mechanism**: Player choices are hidden until both players have acted, built with Jazz group permissions
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js 20+
|
||||||
|
- pnpm (recommended) or npm
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Install dependencies**:
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate environment variables**:
|
||||||
|
```bash
|
||||||
|
pnpm generate-env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the development server**:
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open your browser** to [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
The `generate-env.ts` script creates the necessary environment variables for Jazz:
|
||||||
|
- `NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT`: Server worker account ID
|
||||||
|
- `JAZZ_WORKER_SECRET`: Server worker secret key
|
||||||
|
|
||||||
|
## 🔧 Key Implementation Details
|
||||||
|
|
||||||
|
### Server API Definition
|
||||||
|
```typescript
|
||||||
|
const playRequest = experimental_defineRequest({
|
||||||
|
url: "/api/play",
|
||||||
|
workerId,
|
||||||
|
request: {
|
||||||
|
schema: {
|
||||||
|
game: Game,
|
||||||
|
selection: z.literal(["rock", "paper", "scissors"]),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
game: {
|
||||||
|
player1: { account: true, playSelection: { group: true } },
|
||||||
|
player2: { account: true, playSelection: { group: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
schema: { game: Game },
|
||||||
|
resolve: { game: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secure Move Handling
|
||||||
|
```typescript
|
||||||
|
// Create restricted group for the move
|
||||||
|
const group = Group.create({ owner: jazzServerAccount.worker });
|
||||||
|
group.addMember(madeBy, "reader");
|
||||||
|
|
||||||
|
// Store move with restricted access
|
||||||
|
const playSelection = PlaySelection.create(
|
||||||
|
{ value: selection, group },
|
||||||
|
group,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reveal moves only after both players have acted
|
||||||
|
if (player1PlaySelection && player2PlaySelection) {
|
||||||
|
player1PlaySelection.group.addMember(game.player2.account, "reader");
|
||||||
|
player2PlaySelection.group.addMember(game.player1.account, "reader");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Use Cases
|
||||||
|
|
||||||
|
This pattern is ideal for applications requiring:
|
||||||
|
|
||||||
|
- **Fair play guarantees**: Preventing cheating in games
|
||||||
|
- **Simultaneous reveals**: Auctions, voting, or sealed-bid systems
|
||||||
|
- **Trusted computation**: Server-side validation of complex business logic
|
||||||
|
- **Real-time collaboration**: Multi-user applications with strict rules
|
||||||
|
|
||||||
|
## 📚 Learn More
|
||||||
|
|
||||||
|
- [Jazz Documentation](https://jazz.tools/docs)
|
||||||
|
- [Next.js App Router](https://nextjs.org/docs/app)
|
||||||
|
- [Groups & Permissions](https://jazz.tools/docs/react/groups/intro)
|
||||||
|
- [HTTP API with experimental_defineRequest](https://jazz.tools/docs/react/server-side/http-requests)
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
This example is part of the Jazz framework. Feel free to submit issues and enhancement requests!
|
||||||
21
examples/server-worker-http/components.json
Normal file
21
examples/server-worker-http/components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
26
examples/server-worker-http/generate-env.ts
Normal file
26
examples/server-worker-http/generate-env.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { createWorkerAccount } from "jazz-run/createWorkerAccount";
|
||||||
|
|
||||||
|
if (fs.existsSync(".env")) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const account = await createWorkerAccount({
|
||||||
|
name: "jazz-paper-scissors-worker",
|
||||||
|
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
".env",
|
||||||
|
`
|
||||||
|
JAZZ_WORKER_ACCOUNT=${account.accountID}
|
||||||
|
NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT=${account.accountID}
|
||||||
|
JAZZ_WORKER_SECRET=${account.agentSecret}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
5
examples/server-worker-http/next.config.ts
Normal file
5
examples/server-worker-http/next.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
40
examples/server-worker-http/package.json
Normal file
40
examples/server-worker-http/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "server-worker-http",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"generate-env": "tsx generate-env.ts",
|
||||||
|
"build": "pnpm generate-env && next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"jazz-tools": "workspace:*",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"next": "15.3.2",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss": "^4.1.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.50.1",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"jazz-run": "workspace:*",
|
||||||
|
"tsx": "^4.19.3",
|
||||||
|
"tw-animate-css": "^1.2.5",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
examples/server-worker-http/playwright.config.ts
Normal file
55
examples/server-worker-http/playwright.config.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
import isCI from "is-ci";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: isCI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: isCI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: isCI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
|
||||||
|
timeout: 20_000,
|
||||||
|
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "http://localhost:3000/",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
permissions: ["clipboard-read", "clipboard-write"],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: [
|
||||||
|
{
|
||||||
|
command: "pnpm start",
|
||||||
|
url: "http://localhost:3000/",
|
||||||
|
reuseExistingServer: !isCI,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
2
examples/server-worker-http/postcss.config.mjs
Normal file
2
examples/server-worker-http/postcss.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const config = { plugins: { "@tailwindcss/postcss": {} } };
|
||||||
|
export default config;
|
||||||
1
examples/server-worker-http/src/apiKey.ts
Normal file
1
examples/server-worker-http/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const apiKey = "server-side-validation@garden.co";
|
||||||
23
examples/server-worker-http/src/app/api/create-game/route.ts
Normal file
23
examples/server-worker-http/src/app/api/create-game/route.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { jazzServerAccount } from "@/jazzServerAccount";
|
||||||
|
import { WaitingRoom } from "@/schema";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { Group } from "jazz-tools";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const response = await serverApi.createGame.handle(
|
||||||
|
request,
|
||||||
|
jazzServerAccount.worker,
|
||||||
|
async (_, madeBy) => {
|
||||||
|
const waitingRoom = WaitingRoom.create(
|
||||||
|
{ creator: madeBy },
|
||||||
|
Group.create(jazzServerAccount.worker).makePublic(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
waitingRoom,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
49
examples/server-worker-http/src/app/api/join-game/route.ts
Normal file
49
examples/server-worker-http/src/app/api/join-game/route.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { jazzServerAccount } from "@/jazzServerAccount";
|
||||||
|
import { Game, createGameState } from "@/schema";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { Account, Group, JazzRequestError } from "jazz-tools";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
return serverApi.joinGame.handle(
|
||||||
|
request,
|
||||||
|
jazzServerAccount.worker,
|
||||||
|
async ({ waitingRoom }, madeBy) => {
|
||||||
|
if (madeBy.id === waitingRoom.creator.id) {
|
||||||
|
throw new JazzRequestError("You can't join your own waiting room", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitingRoom.game = createGame({
|
||||||
|
account1: waitingRoom.creator,
|
||||||
|
account2: madeBy,
|
||||||
|
worker: jazzServerAccount.worker,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
waitingRoom,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateGameParams {
|
||||||
|
account1: Account;
|
||||||
|
account2: Account;
|
||||||
|
worker: Account;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGame({ account1, account2, worker }: CreateGameParams) {
|
||||||
|
const gameGroup = Group.create({ owner: worker });
|
||||||
|
gameGroup.addMember(account1, "reader");
|
||||||
|
gameGroup.addMember(account2, "reader");
|
||||||
|
|
||||||
|
const game = Game.create(
|
||||||
|
{
|
||||||
|
...createGameState({ account1, account2, worker }),
|
||||||
|
player1Score: 0,
|
||||||
|
player2Score: 0,
|
||||||
|
},
|
||||||
|
gameGroup,
|
||||||
|
);
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
35
examples/server-worker-http/src/app/api/new-game/route.ts
Normal file
35
examples/server-worker-http/src/app/api/new-game/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { jazzServerAccount } from "@/jazzServerAccount";
|
||||||
|
import { createGameState } from "@/schema";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { JazzRequestError } from "jazz-tools";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const response = await serverApi.newGame.handle(
|
||||||
|
request,
|
||||||
|
jazzServerAccount.worker,
|
||||||
|
async ({ game }, madeBy) => {
|
||||||
|
const isPlayer1 = game.player1.account.id === madeBy.id;
|
||||||
|
const isPlayer2 = game.player2.account.id === madeBy.id;
|
||||||
|
|
||||||
|
if (!isPlayer1 && !isPlayer2) {
|
||||||
|
throw new JazzRequestError("You are not a player in this game", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.outcome) {
|
||||||
|
game.applyDiff(
|
||||||
|
createGameState({
|
||||||
|
account1: game.player1.account,
|
||||||
|
account2: game.player2.account,
|
||||||
|
worker: jazzServerAccount.worker,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
game,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
89
examples/server-worker-http/src/app/api/play/route.ts
Normal file
89
examples/server-worker-http/src/app/api/play/route.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { jazzServerAccount } from "@/jazzServerAccount";
|
||||||
|
import { PlaySelection } from "@/schema";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { Group, JazzRequestError } from "jazz-tools";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const response = await serverApi.play.handle(
|
||||||
|
request,
|
||||||
|
jazzServerAccount.worker,
|
||||||
|
async ({ game, selection }, madeBy) => {
|
||||||
|
const isPlayer1 = game.player1.account.id === madeBy.id;
|
||||||
|
const isPlayer2 = game.player2.account.id === madeBy.id;
|
||||||
|
|
||||||
|
if (!isPlayer1 && !isPlayer2) {
|
||||||
|
throw new JazzRequestError("You are not a player in this game", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = Group.create({ owner: jazzServerAccount.worker });
|
||||||
|
group.addMember(madeBy, "reader");
|
||||||
|
|
||||||
|
if (isPlayer1 && game.player1.playSelection) {
|
||||||
|
throw new JazzRequestError("You have already played", 400);
|
||||||
|
} else if (isPlayer2 && game.player2.playSelection) {
|
||||||
|
throw new JazzRequestError("You have already played", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const playSelection = PlaySelection.create(
|
||||||
|
{ value: selection, group },
|
||||||
|
group,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPlayer1) {
|
||||||
|
game.player1.playSelection = playSelection;
|
||||||
|
} else {
|
||||||
|
game.player2.playSelection = playSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.player1.playSelection && game.player2.playSelection) {
|
||||||
|
game.outcome = determineOutcome(
|
||||||
|
game.player1.playSelection.value,
|
||||||
|
game.player2.playSelection.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reveal the play selections to the other player
|
||||||
|
game.player1.playSelection.group.addMember(
|
||||||
|
game.player2.account,
|
||||||
|
"reader",
|
||||||
|
);
|
||||||
|
game.player2.playSelection.group.addMember(
|
||||||
|
game.player1.account,
|
||||||
|
"reader",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (game.outcome === "player1") {
|
||||||
|
game.player1Score++;
|
||||||
|
} else if (game.outcome === "player2") {
|
||||||
|
game.player2Score++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
game,
|
||||||
|
result: "success",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a player selections, returns the winner of the current game.
|
||||||
|
*/
|
||||||
|
function determineOutcome(
|
||||||
|
player1Choice: "rock" | "paper" | "scissors",
|
||||||
|
player2Choice: "rock" | "paper" | "scissors",
|
||||||
|
) {
|
||||||
|
if (player1Choice === player2Choice) {
|
||||||
|
return "draw";
|
||||||
|
} else if (
|
||||||
|
(player1Choice === "rock" && player2Choice === "scissors") ||
|
||||||
|
(player1Choice === "paper" && player2Choice === "rock") ||
|
||||||
|
(player1Choice === "scissors" && player2Choice === "paper")
|
||||||
|
) {
|
||||||
|
return "player1";
|
||||||
|
} else {
|
||||||
|
return "player2";
|
||||||
|
}
|
||||||
|
}
|
||||||
388
examples/server-worker-http/src/app/game/[id]/page.tsx
Normal file
388
examples/server-worker-http/src/app/game/[id]/page.tsx
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Game, PlaySelection, PlayerState } from "@/schema";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { Group, isJazzRequestError } from "jazz-tools";
|
||||||
|
import { useAccount, useCoState } from "jazz-tools/react";
|
||||||
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
Gamepad2,
|
||||||
|
Minus,
|
||||||
|
Sparkles,
|
||||||
|
Trophy,
|
||||||
|
Users,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const playIcon = (
|
||||||
|
selection: "rock" | "paper" | "scissors" | undefined,
|
||||||
|
size: "sm" | "lg" = "sm",
|
||||||
|
) => {
|
||||||
|
const emojiSize = size === "lg" ? "text-4xl" : "text-2xl";
|
||||||
|
|
||||||
|
switch (selection) {
|
||||||
|
case "rock":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`${emojiSize} `} style={{ animationDelay: "0ms" }}>
|
||||||
|
🪨
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">Rock</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "paper":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`${emojiSize} `} style={{ animationDelay: "150ms" }}>
|
||||||
|
📄
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">Paper</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "scissors":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`${emojiSize} `} style={{ animationDelay: "300ms" }}>
|
||||||
|
✂️
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">Scissors</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Clock className={size === "lg" ? "w-8 h-8" : "w-6 h-6"} />
|
||||||
|
<span>Waiting for selection</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOutcomeIcon = (outcome: string | undefined, player: string) => {
|
||||||
|
if (outcome === player) {
|
||||||
|
return <Trophy className="w-6 h-6 text-yellow-500" />;
|
||||||
|
} else if (outcome === "draw") {
|
||||||
|
return <Minus className="w-6 h-6 text-blue-500" />;
|
||||||
|
} else {
|
||||||
|
return <XCircle className="w-6 h-6 text-red-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RouteComponent() {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const game = useCoState(Game, params.id, {
|
||||||
|
resolve: {
|
||||||
|
player1State: {
|
||||||
|
$onError: null,
|
||||||
|
},
|
||||||
|
player2State: {
|
||||||
|
$onError: null,
|
||||||
|
},
|
||||||
|
player1: {
|
||||||
|
account: true,
|
||||||
|
playSelection: {
|
||||||
|
$onError: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
player2: {
|
||||||
|
account: true,
|
||||||
|
playSelection: {
|
||||||
|
$onError: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPlayer1 = game?.player1?.account?.isMe;
|
||||||
|
const player = isPlayer1 ? "player1" : "player2";
|
||||||
|
|
||||||
|
if (!game) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<Gamepad2 className="w-12 h-12 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameComplete = game.outcome !== undefined;
|
||||||
|
|
||||||
|
const opponent = isPlayer1 ? "player2" : "player1";
|
||||||
|
|
||||||
|
const currentPlayer = game[player];
|
||||||
|
const opponentPlayer = game[opponent];
|
||||||
|
|
||||||
|
const currentPlayerState = game[isPlayer1 ? "player1State" : "player2State"];
|
||||||
|
|
||||||
|
const opponentSelection = opponentPlayer?.playSelection;
|
||||||
|
const opponentHasSelected = Boolean(opponentPlayer._refs.playSelection);
|
||||||
|
|
||||||
|
const handleSelection = (selection: "rock" | "paper" | "scissors") => {
|
||||||
|
if (!currentPlayerState) return;
|
||||||
|
if (currentPlayerState.submitted) return;
|
||||||
|
|
||||||
|
currentPlayerState.currentSelection = selection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
playSelection: "rock" | "paper" | "scissors" | undefined,
|
||||||
|
) => {
|
||||||
|
if (!playSelection) return;
|
||||||
|
if (!currentPlayerState) return;
|
||||||
|
|
||||||
|
currentPlayerState.submitted = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await serverApi.play.send({
|
||||||
|
game,
|
||||||
|
selection: playSelection,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
currentPlayerState.submitted = false;
|
||||||
|
if (isJazzRequestError(error)) {
|
||||||
|
toast.error(error.message);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewGame = async () => {
|
||||||
|
if (!currentPlayerState) return;
|
||||||
|
|
||||||
|
currentPlayerState.resetRequested = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await serverApi.newGame.send({
|
||||||
|
game,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
currentPlayerState.resetRequested = false;
|
||||||
|
|
||||||
|
if (isJazzRequestError(error)) {
|
||||||
|
toast.error(error.message);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playSelection = currentPlayerState?.currentSelection;
|
||||||
|
|
||||||
|
const submitDisabled =
|
||||||
|
!currentPlayerState?.currentSelection || currentPlayerState?.submitted;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-8 px-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-4">
|
||||||
|
<Gamepad2 className="w-8 h-8 text-primary" />
|
||||||
|
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent">
|
||||||
|
Rock, Paper, Scissors
|
||||||
|
</h1>
|
||||||
|
<Sparkles className="w-6 h-6 text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Player Info */}
|
||||||
|
<div className="flex items-center justify-center gap-4 mb-6">
|
||||||
|
<Badge variant="secondary" className="px-4 py-2">
|
||||||
|
<Users className="w-4 h-4 mr-2" />
|
||||||
|
{isPlayer1 ? "Player 1" : "Player 2"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score Board */}
|
||||||
|
<Card className="inline-block bg-white/80 backdrop-blur-sm border-0 shadow-lg">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-6 text-2xl font-bold">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-blue-600">Player 1</span>
|
||||||
|
<span className="text-3xl">{game?.player1Score ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">-</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-3xl">{game?.player2Score ?? 0}</span>
|
||||||
|
<span className="text-purple-600">Player 2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Game Outcome */}
|
||||||
|
{gameComplete && (
|
||||||
|
<Card className="mb-8 bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200">
|
||||||
|
<CardContent className="p-6 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-4">
|
||||||
|
{getOutcomeIcon(game.outcome, player)}
|
||||||
|
<h2 className="text-2xl font-bold">
|
||||||
|
{game?.outcome === player
|
||||||
|
? "🎉 You Win! 🎉"
|
||||||
|
: game?.outcome === "draw"
|
||||||
|
? "🤝 It's a Draw! 🤝"
|
||||||
|
: "😔 You Lose! 😔"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
disabled={currentPlayerState?.resetRequested}
|
||||||
|
onClick={handleNewGame}
|
||||||
|
className="bg-gradient-to-r from-primary to-purple-600 hover:from-primary/90 hover:to-purple-600/90 text-white"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Gamepad2 className="w-5 h-5 mr-2" />
|
||||||
|
Start New Game
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Game Board */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{/* Your Selection */}
|
||||||
|
<Card className="bg-white/80 backdrop-blur-sm border-0 shadow-lg">
|
||||||
|
<CardHeader className="text-center pb-4">
|
||||||
|
<CardTitle className="text-xl font-semibold text-blue-600">
|
||||||
|
Your Selection
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-center">
|
||||||
|
<div className="mb-6">{playIcon(playSelection, "lg")}</div>
|
||||||
|
|
||||||
|
{!gameComplete && (
|
||||||
|
<>
|
||||||
|
{/* Choice Buttons */}
|
||||||
|
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||||
|
<Button
|
||||||
|
variant={playSelection === "rock" ? "default" : "outline"}
|
||||||
|
size="lg"
|
||||||
|
className={`h-20 transition-all duration-200 ${
|
||||||
|
playSelection === "rock"
|
||||||
|
? "bg-gradient-to-br from-gray-400 to-gray-600 text-white shadow-lg scale-105"
|
||||||
|
: "hover:scale-105"
|
||||||
|
} text-3xl`}
|
||||||
|
onClick={() => handleSelection("rock")}
|
||||||
|
aria-label="Select Rock"
|
||||||
|
aria-selected={playSelection === "rock"}
|
||||||
|
>
|
||||||
|
🪨
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
playSelection === "paper" ? "default" : "outline"
|
||||||
|
}
|
||||||
|
size="lg"
|
||||||
|
className={`h-20 transition-all duration-200 ${
|
||||||
|
playSelection === "paper"
|
||||||
|
? "bg-gradient-to-br from-blue-400 to-blue-600 text-white shadow-lg scale-105"
|
||||||
|
: "hover:scale-105"
|
||||||
|
} text-3xl`}
|
||||||
|
onClick={() => handleSelection("paper")}
|
||||||
|
aria-label="Select Paper"
|
||||||
|
aria-selected={playSelection === "paper"}
|
||||||
|
>
|
||||||
|
📄
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
playSelection === "scissors" ? "default" : "outline"
|
||||||
|
}
|
||||||
|
size="lg"
|
||||||
|
className={`h-20 transition-all duration-200 ${
|
||||||
|
playSelection === "scissors"
|
||||||
|
? "bg-gradient-to-br from-red-400 to-red-600 text-white shadow-lg scale-105"
|
||||||
|
: "hover:scale-105"
|
||||||
|
} text-3xl`}
|
||||||
|
onClick={() => handleSelection("scissors")}
|
||||||
|
aria-label="Select Scissors"
|
||||||
|
aria-selected={playSelection === "scissors"}
|
||||||
|
>
|
||||||
|
✂️
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Button
|
||||||
|
disabled={submitDisabled}
|
||||||
|
onClick={() => handleSubmit(playSelection)}
|
||||||
|
className="w-full bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white font-semibold py-3 text-lg"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{currentPlayerState?.submitted ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-5 h-5 mr-2" />
|
||||||
|
Selection Made!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Gamepad2 className="w-5 h-5 mr-2" />
|
||||||
|
Make Your Move!
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Opponent Selection */}
|
||||||
|
<Card className="bg-white/80 backdrop-blur-sm border-0 shadow-lg">
|
||||||
|
<CardHeader className="text-center pb-4">
|
||||||
|
<CardTitle className="text-xl font-semibold text-purple-600">
|
||||||
|
Opponent's Selection
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-center">
|
||||||
|
{opponentSelection && (
|
||||||
|
<div className="mb-6">
|
||||||
|
{playIcon(opponentSelection?.value, "lg")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!opponentSelection && !gameComplete ? (
|
||||||
|
opponentHasSelected ? (
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
<Clock className="w-8 h-8 mx-auto mb-2 animate-pulse" />
|
||||||
|
<p>The opponent has made their move</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
<Clock className="w-8 h-8 mx-auto mb-2 animate-pulse" />
|
||||||
|
<p>Waiting for opponent...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Game Status */}
|
||||||
|
{!gameComplete && (
|
||||||
|
<Card className="mt-8 bg-white/60 backdrop-blur-sm border-0">
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2 text-muted-foreground">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span className="text-sm">
|
||||||
|
{currentPlayer?.playSelection && !opponentSelection
|
||||||
|
? "Waiting for opponent to make their move..."
|
||||||
|
: "Make your selection to start the game"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
examples/server-worker-http/src/app/globals.css
Normal file
120
examples/server-worker-http/src/app/globals.css
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
examples/server-worker-http/src/app/layout.tsx
Normal file
37
examples/server-worker-http/src/app/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { Jazz } from "../jazz";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Jazz Server Side Validation",
|
||||||
|
description: "An example of server side validation with Jazz",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<Jazz>{children}</Jazz>
|
||||||
|
<Toaster />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
examples/server-worker-http/src/app/page.tsx
Normal file
97
examples/server-worker-http/src/app/page.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function HomeComponent() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onNewGameClick = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const { waitingRoom } = await serverApi.createGame.send({});
|
||||||
|
|
||||||
|
router.push(`/waiting-room/${waitingRoom.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900 flex flex-col items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
{/* Background decoration */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="relative z-10 text-center space-y-8 max-w-2xl mx-auto">
|
||||||
|
{/* Game title and emojis */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-center items-center space-x-4 text-6xl mb-6">
|
||||||
|
<span className="animate-bounce" style={{ animationDelay: "0ms" }}>
|
||||||
|
🪨
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="animate-bounce"
|
||||||
|
style={{ animationDelay: "150ms" }}
|
||||||
|
>
|
||||||
|
📄
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="animate-bounce"
|
||||||
|
style={{ animationDelay: "300ms" }}
|
||||||
|
>
|
||||||
|
✂️
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-5xl font-bold text-white mb-2">
|
||||||
|
Rock, Paper, Scissors
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-300 max-w-md mx-auto">
|
||||||
|
Challenge your friends in this classic multiplayer game powered by
|
||||||
|
Jazz
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Game card */}
|
||||||
|
<Card className="w-full max-w-md mx-auto bg-white/10 backdrop-blur-lg border-white/20 shadow-2xl">
|
||||||
|
<CardHeader className="text-center pb-4">
|
||||||
|
<CardTitle className="text-2xl font-bold text-white">
|
||||||
|
Ready to Play?
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-gray-300 text-sm mt-2">
|
||||||
|
Create a new game and invite your friends
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Button
|
||||||
|
onClick={onNewGameClick}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full h-12 text-lg font-semibold bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
<span>Creating game...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>🎮</span>
|
||||||
|
<span>Start New Game</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="text-center text-gray-400 text-sm mt-12">
|
||||||
|
<p>Built with Jazz Framework</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
173
examples/server-worker-http/src/app/waiting-room/[id]/page.tsx
Normal file
173
examples/server-worker-http/src/app/waiting-room/[id]/page.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { WaitingRoom } from "@/schema";
|
||||||
|
import { serverApi } from "@/serverApi";
|
||||||
|
import { Account, JazzRequestError, co } from "jazz-tools";
|
||||||
|
import { useCoState } from "jazz-tools/react-core";
|
||||||
|
import { ClipboardCopyIcon, Loader2Icon } from "lucide-react";
|
||||||
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
function useWindowLocation() {
|
||||||
|
const [location, setLocation] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocation(window.location.href);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askToJoinGame(
|
||||||
|
waitingRoom: co.loaded<typeof WaitingRoom, { creator: true }>,
|
||||||
|
) {
|
||||||
|
if (waitingRoom.creator.isMe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await serverApi.joinGame.send({
|
||||||
|
waitingRoom,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof JazzRequestError) {
|
||||||
|
toast.error(error.message);
|
||||||
|
} else {
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RouteComponent() {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const waitingRoom = useCoState(WaitingRoom, params.id, {
|
||||||
|
resolve: {
|
||||||
|
creator: true,
|
||||||
|
game: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const location = useWindowLocation();
|
||||||
|
const joinLocation = location + "?join=true";
|
||||||
|
|
||||||
|
const isJoining = !waitingRoom
|
||||||
|
? searchParams.get("join") === "true"
|
||||||
|
: Account.getMe().id !== waitingRoom.creator.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waitingRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
askToJoinGame(waitingRoom);
|
||||||
|
}, [waitingRoom?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waitingRoom?.game?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(`/game/${waitingRoom.game.id}`);
|
||||||
|
}, [waitingRoom?.game?.id]);
|
||||||
|
|
||||||
|
const onCopyClick = () => {
|
||||||
|
navigator.clipboard.writeText(joinLocation);
|
||||||
|
setCopied(true);
|
||||||
|
toast.success("Link copied to clipboard!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900 flex flex-col items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
{/* Background decoration */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="relative z-10 text-center space-y-8 max-w-2xl mx-auto">
|
||||||
|
{/* Game title and emojis */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-center items-center space-x-4 text-6xl mb-6">
|
||||||
|
<span className="animate-bounce" style={{ animationDelay: "0ms" }}>
|
||||||
|
🪨
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="animate-bounce"
|
||||||
|
style={{ animationDelay: "150ms" }}
|
||||||
|
>
|
||||||
|
📄
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="animate-bounce"
|
||||||
|
style={{ animationDelay: "300ms" }}
|
||||||
|
>
|
||||||
|
✂️
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-5xl font-bold text-white mb-2">Waiting Room</h1>
|
||||||
|
{!isJoining && (
|
||||||
|
<p className="text-xl text-gray-300 max-w-md mx-auto">
|
||||||
|
Share this link with your friend to join the game
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Waiting room card */}
|
||||||
|
<Card className="w-full max-w-md mx-auto bg-white/10 backdrop-blur-lg border-white/20 shadow-2xl">
|
||||||
|
<CardHeader className="text-center pb-4">
|
||||||
|
<CardTitle className="text-2xl font-bold text-white flex items-center justify-center">
|
||||||
|
<Loader2Icon className="animate-spin inline h-8 w-8 mr-3" />
|
||||||
|
{isJoining ? "Joining the game" : "Waiting for opponent"}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-gray-300 text-sm mt-2">
|
||||||
|
The game will automatically start once{" "}
|
||||||
|
{isJoining ? "ready" : "they join"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{!isJoining && (
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex">
|
||||||
|
<Input
|
||||||
|
className="w-full border-white/20 bg-white/5 text-white placeholder:text-gray-400 rounded-e-none focus:border-white/40 focus:ring-white/20"
|
||||||
|
readOnly
|
||||||
|
value={joinLocation}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={onCopyClick}
|
||||||
|
className="rounded-s-none w-25 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
"Copied!"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ClipboardCopyIcon className="w-5 h-5" />
|
||||||
|
Copy
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="text-center text-gray-400 text-sm mt-12">
|
||||||
|
<p>Built with Jazz Framework</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
examples/server-worker-http/src/components/ui/alert.tsx
Normal file
66
examples/server-worker-http/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
46
examples/server-worker-http/src/components/ui/badge.tsx
Normal file
46
examples/server-worker-http/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
59
examples/server-worker-http/src/components/ui/button.tsx
Normal file
59
examples/server-worker-http/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
92
examples/server-worker-http/src/components/ui/card.tsx
Normal file
92
examples/server-worker-http/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
24
examples/server-worker-http/src/components/ui/label.tsx
Normal file
24
examples/server-worker-http/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label };
|
||||||
28
examples/server-worker-http/src/components/ui/separator.tsx
Normal file
28
examples/server-worker-http/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
25
examples/server-worker-http/src/components/ui/sonner.tsx
Normal file
25
examples/server-worker-http/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner";
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toaster };
|
||||||
15
examples/server-worker-http/src/jazz.tsx
Normal file
15
examples/server-worker-http/src/jazz.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { JazzReactProvider } from "jazz-tools/react";
|
||||||
|
import { apiKey } from "./apiKey";
|
||||||
|
|
||||||
|
export function Jazz({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<JazzReactProvider
|
||||||
|
enableSSR
|
||||||
|
sync={{
|
||||||
|
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</JazzReactProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
examples/server-worker-http/src/jazzServerAccount.ts
Normal file
5
examples/server-worker-http/src/jazzServerAccount.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { startWorker } from "jazz-tools/worker";
|
||||||
|
|
||||||
|
export const jazzServerAccount = await startWorker({
|
||||||
|
syncServer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co ",
|
||||||
|
});
|
||||||
6
examples/server-worker-http/src/lib/utils.ts
Normal file
6
examples/server-worker-http/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
94
examples/server-worker-http/src/schema.ts
Normal file
94
examples/server-worker-http/src/schema.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Account, Group, co, z } from "jazz-tools";
|
||||||
|
|
||||||
|
export const PlayerState = co.map({
|
||||||
|
currentSelection: z.literal(["rock", "paper", "scissors"]).optional(),
|
||||||
|
submitted: z.boolean(),
|
||||||
|
resetRequested: z.boolean(),
|
||||||
|
});
|
||||||
|
export type PlayerState = co.loaded<typeof PlayerState>;
|
||||||
|
|
||||||
|
export const PlaySelection = co.map({
|
||||||
|
value: z.literal(["rock", "paper", "scissors"]),
|
||||||
|
group: Group,
|
||||||
|
});
|
||||||
|
export type PlaySelection = co.loaded<typeof PlaySelection>;
|
||||||
|
|
||||||
|
export const Player = co.map({
|
||||||
|
account: co.account(),
|
||||||
|
playSelection: PlaySelection.optional(),
|
||||||
|
});
|
||||||
|
export type Player = co.loaded<typeof Player>;
|
||||||
|
|
||||||
|
export const Game = co.map({
|
||||||
|
player1: Player,
|
||||||
|
player2: Player,
|
||||||
|
player1State: PlayerState,
|
||||||
|
player2State: PlayerState,
|
||||||
|
outcome: z.literal(["player1", "player2", "draw"]).optional(),
|
||||||
|
player1Score: z.number(),
|
||||||
|
player2Score: z.number(),
|
||||||
|
});
|
||||||
|
export type Game = co.loaded<typeof Game>;
|
||||||
|
|
||||||
|
export const WaitingRoom = co.map({
|
||||||
|
creator: co.account(),
|
||||||
|
game: co.optional(Game),
|
||||||
|
});
|
||||||
|
export type WaitingRoom = co.loaded<typeof WaitingRoom>;
|
||||||
|
|
||||||
|
export function createGameState(params: {
|
||||||
|
account1: Account;
|
||||||
|
account2: Account;
|
||||||
|
worker: Account;
|
||||||
|
}) {
|
||||||
|
const { account1, account2, worker } = params;
|
||||||
|
const gameGroup = Group.create({ owner: worker });
|
||||||
|
gameGroup.addMember(account1, "reader");
|
||||||
|
gameGroup.addMember(account2, "reader");
|
||||||
|
|
||||||
|
const player1 = Player.create(
|
||||||
|
{
|
||||||
|
account: account1,
|
||||||
|
},
|
||||||
|
gameGroup,
|
||||||
|
);
|
||||||
|
|
||||||
|
const player2 = Player.create(
|
||||||
|
{
|
||||||
|
account: account2,
|
||||||
|
},
|
||||||
|
gameGroup,
|
||||||
|
);
|
||||||
|
|
||||||
|
const player1StateGroup = Group.create(worker);
|
||||||
|
player1StateGroup.addMember(account1, "writer");
|
||||||
|
|
||||||
|
const player1State = PlayerState.create(
|
||||||
|
{
|
||||||
|
currentSelection: undefined,
|
||||||
|
submitted: false,
|
||||||
|
resetRequested: false,
|
||||||
|
},
|
||||||
|
player1StateGroup,
|
||||||
|
);
|
||||||
|
|
||||||
|
const player2StateGroup = Group.create(worker);
|
||||||
|
player2StateGroup.addMember(account2, "writer");
|
||||||
|
|
||||||
|
const player2State = PlayerState.create(
|
||||||
|
{
|
||||||
|
currentSelection: undefined,
|
||||||
|
submitted: false,
|
||||||
|
resetRequested: false,
|
||||||
|
},
|
||||||
|
player2StateGroup,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
player1,
|
||||||
|
player2,
|
||||||
|
player1State,
|
||||||
|
player2State,
|
||||||
|
outcome: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
125
examples/server-worker-http/src/serverApi.ts
Normal file
125
examples/server-worker-http/src/serverApi.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { experimental_defineRequest, z } from "jazz-tools";
|
||||||
|
import { Game, WaitingRoom } from "./schema";
|
||||||
|
|
||||||
|
const workerId = process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT!;
|
||||||
|
|
||||||
|
const createGameRequest = experimental_defineRequest({
|
||||||
|
url: "/api/create-game",
|
||||||
|
workerId,
|
||||||
|
request: {},
|
||||||
|
response: {
|
||||||
|
schema: { waitingRoom: WaitingRoom },
|
||||||
|
resolve: { waitingRoom: { creator: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinGameRequest = experimental_defineRequest({
|
||||||
|
url: "/api/join-game",
|
||||||
|
workerId,
|
||||||
|
request: {
|
||||||
|
schema: {
|
||||||
|
waitingRoom: WaitingRoom,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
waitingRoom: {
|
||||||
|
creator: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
schema: {
|
||||||
|
waitingRoom: WaitingRoom,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
waitingRoom: {
|
||||||
|
game: {
|
||||||
|
player1: {
|
||||||
|
account: true,
|
||||||
|
},
|
||||||
|
player2: {
|
||||||
|
account: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newGameRequest = experimental_defineRequest({
|
||||||
|
url: "/api/new-game",
|
||||||
|
workerId,
|
||||||
|
request: {
|
||||||
|
schema: {
|
||||||
|
game: Game,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
game: {
|
||||||
|
player1: {
|
||||||
|
account: true,
|
||||||
|
},
|
||||||
|
player2: {
|
||||||
|
account: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
schema: {
|
||||||
|
game: Game,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
game: {
|
||||||
|
player1State: {
|
||||||
|
$onError: null,
|
||||||
|
},
|
||||||
|
player2State: {
|
||||||
|
$onError: null,
|
||||||
|
},
|
||||||
|
player1: true,
|
||||||
|
player2: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const playRequest = experimental_defineRequest({
|
||||||
|
url: "/api/play",
|
||||||
|
workerId,
|
||||||
|
request: {
|
||||||
|
schema: {
|
||||||
|
game: Game,
|
||||||
|
selection: z.literal(["rock", "paper", "scissors"]),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
game: {
|
||||||
|
player1: {
|
||||||
|
account: true,
|
||||||
|
playSelection: {
|
||||||
|
group: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
player2: {
|
||||||
|
account: true,
|
||||||
|
playSelection: {
|
||||||
|
group: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
schema: {
|
||||||
|
game: Game,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
game: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const serverApi = {
|
||||||
|
createGame: createGameRequest,
|
||||||
|
joinGame: joinGameRequest,
|
||||||
|
newGame: newGameRequest,
|
||||||
|
play: playRequest,
|
||||||
|
};
|
||||||
37
examples/server-worker-http/tests/play.spec.ts
Normal file
37
examples/server-worker-http/tests/play.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("start a new game and play", async ({ page: marioPage, browser }) => {
|
||||||
|
await marioPage.goto("/");
|
||||||
|
|
||||||
|
await marioPage.getByRole("button", { name: /Start New Game/ }).click();
|
||||||
|
|
||||||
|
await expect(marioPage.getByText("Waiting for opponent")).toBeVisible();
|
||||||
|
|
||||||
|
const url = await marioPage.url();
|
||||||
|
|
||||||
|
const luigiContext = await browser.newContext();
|
||||||
|
const luigiPage = await luigiContext.newPage();
|
||||||
|
await luigiPage.goto(url);
|
||||||
|
|
||||||
|
await expect(marioPage.getByText("Waiting for selection")).toBeVisible();
|
||||||
|
await expect(luigiPage.getByText("Waiting for selection")).toBeVisible();
|
||||||
|
await expect(luigiPage.getByText("Waiting for opponent")).toBeVisible();
|
||||||
|
|
||||||
|
await marioPage.getByLabel("Select Rock").click();
|
||||||
|
await marioPage.getByRole("button", { name: /Make your move!/i }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
luigiPage.getByText("The opponent has made their move"),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await luigiPage.getByLabel("Select Paper").click();
|
||||||
|
await luigiPage.getByRole("button", { name: /Make your move!/i }).click();
|
||||||
|
|
||||||
|
await expect(luigiPage.getByText(/🎉 You Win! 🎉/)).toBeVisible();
|
||||||
|
await expect(marioPage.getByText(/😔 You Lose! 😔/)).toBeVisible();
|
||||||
|
|
||||||
|
await marioPage.getByRole("button", { name: /Start New Game/ }).click();
|
||||||
|
|
||||||
|
await expect(marioPage.getByText("Waiting for opponent")).toBeVisible();
|
||||||
|
await expect(luigiPage.getByText("Waiting for opponent")).toBeVisible();
|
||||||
|
});
|
||||||
27
examples/server-worker-http/tsconfig.json
Normal file
27
examples/server-worker-http/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -24,15 +24,15 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.485.0",
|
"lucide-react": "^0.485.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.17",
|
"tailwindcss": "^4.0.17",
|
||||||
"tw-animate-css": "^1.2.5"
|
"tw-animate-css": "^1.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"jazz-run": "workspace:*",
|
"jazz-run": "workspace:*",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user