Compare commits
697 Commits
jazz-examp
...
feature/op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
531ca9665a | ||
|
|
207bea3c1c | ||
|
|
fcae20d77c | ||
|
|
0a542941ad | ||
|
|
5dceef8c03 | ||
|
|
fb01eb42f1 | ||
|
|
e1e3a8352e | ||
|
|
09678c4277 | ||
|
|
5b83669368 | ||
|
|
30c8c922a3 | ||
|
|
e05b327eb5 | ||
|
|
06665cbbf7 | ||
|
|
c0425df2ee | ||
|
|
42cbef8063 | ||
|
|
d1abf06621 | ||
|
|
1c00020859 | ||
|
|
00dd9cea99 | ||
|
|
607cd0ab97 | ||
|
|
9f65e56b21 | ||
|
|
82c5d1bf5c | ||
|
|
deda3e571f | ||
|
|
2d4039c5d2 | ||
|
|
63dd9190a2 | ||
|
|
3dfa630923 | ||
|
|
63daf6af09 | ||
|
|
c85ccd96aa | ||
|
|
403b430ee4 | ||
|
|
f58845ef74 | ||
|
|
38fad35945 | ||
|
|
2533740a7c | ||
|
|
109a7d6128 | ||
|
|
ec1906e262 | ||
|
|
6f8028253d | ||
|
|
0704a76006 | ||
|
|
ab93a0b679 | ||
|
|
ed85560547 | ||
|
|
a242adf3b3 | ||
|
|
1a9c7ecefd | ||
|
|
aded528a27 | ||
|
|
152ae9865e | ||
|
|
8bd92b44f5 | ||
|
|
8d15e04fd0 | ||
|
|
b7f3ece0f1 | ||
|
|
4c94ae3da2 | ||
|
|
cd23ce0a51 | ||
|
|
8538036af7 | ||
|
|
40287a5682 | ||
|
|
2ddce1a2de | ||
|
|
49a8b54a8a | ||
|
|
04b15c7d4a | ||
|
|
5909e7e894 | ||
|
|
7a3d519970 | ||
|
|
35bbcd94e6 | ||
|
|
f6629b2b58 | ||
|
|
b4891db9fa | ||
|
|
4e16575f97 | ||
|
|
12ab20ecd9 | ||
|
|
2c3a40c94b | ||
|
|
0d8175ba1c | ||
|
|
311ed74709 | ||
|
|
c86301fcfd | ||
|
|
032f69f692 | ||
|
|
6dc52b2a6d | ||
|
|
1232c0240a | ||
|
|
55fa74f44a | ||
|
|
627e04151d | ||
|
|
5d91f9f8dc | ||
|
|
08f1f77834 | ||
|
|
ea882aba63 | ||
|
|
513a78ab9b | ||
|
|
406ab9b0da | ||
|
|
140f6616cb | ||
|
|
b09589b15e | ||
|
|
00638897f4 | ||
|
|
4cd68c1930 | ||
|
|
f1f3607e28 | ||
|
|
7b9f503b9e | ||
|
|
a18f44399c | ||
|
|
5094e6d536 | ||
|
|
39242e7f68 | ||
|
|
be7e208b1c | ||
|
|
c3bffbf4de | ||
|
|
d46467f318 | ||
|
|
6d21400803 | ||
|
|
db53161296 | ||
|
|
cb4a116cec | ||
|
|
013199b9b2 | ||
|
|
a8b74ff703 | ||
|
|
b1985a9161 | ||
|
|
3bf512719f | ||
|
|
d83ed69d41 | ||
|
|
fdde8db664 | ||
|
|
dd5581ba2d | ||
|
|
07fe2b9dcf | ||
|
|
b297c93b80 | ||
|
|
d2e62e5b44 | ||
|
|
fe73ce7514 | ||
|
|
0fed16cea4 | ||
|
|
08804ad435 | ||
|
|
79fa7724ad | ||
|
|
4604c2184a | ||
|
|
11bac697fb | ||
|
|
96cec27f89 | ||
|
|
bcd412b8f9 | ||
|
|
6b456e2841 | ||
|
|
1df72b3dc8 | ||
|
|
402d692739 | ||
|
|
fad46b2fb5 | ||
|
|
0153c80cf2 | ||
|
|
4f75dc8d97 | ||
|
|
e2c79cccb5 | ||
|
|
c14a0e05be | ||
|
|
016dd3a5dd | ||
|
|
5c4ca9103c | ||
|
|
b4aad92907 | ||
|
|
56d1e095a1 | ||
|
|
6dee9aae49 | ||
|
|
a10bff981e | ||
|
|
e333f7884a | ||
|
|
8ea7bf237b | ||
|
|
5e8409fa08 | ||
|
|
23354c1767 | ||
|
|
0efb69d0db | ||
|
|
0462c4e41b | ||
|
|
70a5673197 | ||
|
|
9ec3203485 | ||
|
|
1a46f9b2e1 | ||
|
|
77bb26a8d7 | ||
|
|
2a36dcf592 | ||
|
|
fc2bcadbe2 | ||
|
|
46b0cc1adb | ||
|
|
d75d1c6a3f | ||
|
|
13b236aeed | ||
|
|
1c0a61b0b2 | ||
|
|
ceb92438f4 | ||
|
|
9bdd62ed4c | ||
|
|
3f5ef7e799 | ||
|
|
e7a573fa94 | ||
|
|
364060eaa7 | ||
|
|
a3ddc3d5e0 | ||
|
|
185f747adb | ||
|
|
895d281088 | ||
|
|
b44e4354f7 | ||
|
|
3fcb0665ec | ||
|
|
be49d33ce5 | ||
|
|
c7dae1608b | ||
|
|
b020c5868b | ||
|
|
eae42d3afe | ||
|
|
a816e2436e | ||
|
|
b09e35e372 | ||
|
|
d2c8121c9c | ||
|
|
380bb88ffa | ||
|
|
e0e3726b3c | ||
|
|
c2253a7979 | ||
|
|
9d244226ec | ||
|
|
71df5e3a59 | ||
|
|
3a738dad88 | ||
|
|
56d301cfde | ||
|
|
5efec6d5ea | ||
|
|
32769b24f1 | ||
|
|
6ab53c263d | ||
|
|
e7f3e4e242 | ||
|
|
8bb5201647 | ||
|
|
a9fc94f53d | ||
|
|
ca7c0510d1 | ||
|
|
1bf16f0859 | ||
|
|
21b503c188 | ||
|
|
0053e9796c | ||
|
|
e84941b1b1 | ||
|
|
57f6f8d67e | ||
|
|
5b8e69d973 | ||
|
|
7213b1bfa3 | ||
|
|
11f0770f08 | ||
|
|
44e6dc3ae8 | ||
|
|
b5d20d2488 | ||
|
|
0185545838 | ||
|
|
8c8f85859c | ||
|
|
104384409e | ||
|
|
179827ae56 | ||
|
|
6645829876 | ||
|
|
68cb302722 | ||
|
|
8dc33f2790 | ||
|
|
5f64ba326c | ||
|
|
7ccb15107c | ||
|
|
b102964743 | ||
|
|
216d50a09c | ||
|
|
07ea59fdcb | ||
|
|
932a84a47f | ||
|
|
34dda7bdbd | ||
|
|
49fa153581 | ||
|
|
c80b827775 | ||
|
|
a2bf9f988a | ||
|
|
ac27b2d5c2 | ||
|
|
c813518fdc | ||
|
|
d5034ed5c3 | ||
|
|
cf2c29a365 | ||
|
|
d948823db6 | ||
|
|
060ad4630d | ||
|
|
0ddceac4c0 | ||
|
|
a862cb8819 | ||
|
|
4246aed7db | ||
|
|
41554e0e0b | ||
|
|
93c4d8155e | ||
|
|
24eefd49f1 | ||
|
|
e712f1e8ef | ||
|
|
33db0fd654 | ||
|
|
478ded93de | ||
|
|
89ad1fb79d | ||
|
|
1ba40806ec | ||
|
|
73ae281e4a | ||
|
|
a35353c987 | ||
|
|
1cb91003cc | ||
|
|
d850022491 | ||
|
|
93792ab6f6 | ||
|
|
95dfe7af6a | ||
|
|
734258eb17 | ||
|
|
f3bcf96fad | ||
|
|
5cf0bc1911 | ||
|
|
d32a6b275f | ||
|
|
6caba9f8e7 | ||
|
|
641f1dbfbe | ||
|
|
58d9a104d6 | ||
|
|
7b9d24c8ef | ||
|
|
4225fdd537 | ||
|
|
9fdc91c6de | ||
|
|
93d8c85e5c | ||
|
|
929cddc3c3 | ||
|
|
e0bc63f016 | ||
|
|
b29ac306ea | ||
|
|
e8e883f4d6 | ||
|
|
3325ff1cd6 | ||
|
|
4fe14f03b4 | ||
|
|
90e2a661e4 | ||
|
|
6ed53ecb79 | ||
|
|
c18775766c | ||
|
|
4bb3a6209a | ||
|
|
0f44a547a4 | ||
|
|
1e2f6d8f14 | ||
|
|
7e5b176930 | ||
|
|
b420eab503 | ||
|
|
b370e2e14e | ||
|
|
1fabee297d | ||
|
|
484dc460c5 | ||
|
|
0cb8756124 | ||
|
|
95d0f0221b | ||
|
|
0c9c0fcd60 | ||
|
|
8be0dd133c | ||
|
|
e68e0ada0d | ||
|
|
49a7349e4d | ||
|
|
979c7241a4 | ||
|
|
e011e4a049 | ||
|
|
92bccf5974 | ||
|
|
2c1d6dcb8f | ||
|
|
75f45ec0b2 | ||
|
|
c0ebcadda9 | ||
|
|
109e9b6a5b | ||
|
|
d0c3d08e42 | ||
|
|
7514830edb | ||
|
|
6f27128b87 | ||
|
|
376518d4ef | ||
|
|
272fc85c13 | ||
|
|
579e4f93ee | ||
|
|
a9accdc3a8 | ||
|
|
b403db51d4 | ||
|
|
108cae037f | ||
|
|
c51897ab9e | ||
|
|
67171cb07a | ||
|
|
6ae3bf6ac9 | ||
|
|
b64b15877a | ||
|
|
19f52b7361 | ||
|
|
9dcd15dbc8 | ||
|
|
a423eeea3b | ||
|
|
99be6a3566 | ||
|
|
e97f730c0f | ||
|
|
f5642335ff | ||
|
|
bf0f8ec824 | ||
|
|
fb9ef4ea20 | ||
|
|
86363197cd | ||
|
|
e93187c971 | ||
|
|
1a86e13cf1 | ||
|
|
b863c1d20a | ||
|
|
4c8d658c25 | ||
|
|
69d37437ef | ||
|
|
220bdbae62 | ||
|
|
b23b556b79 | ||
|
|
ce721cf3d1 | ||
|
|
41363415fe | ||
|
|
b91d0769d5 | ||
|
|
ad16690826 | ||
|
|
ca7b81c36a | ||
|
|
a632ce1477 | ||
|
|
ca7011e9af | ||
|
|
34df432ee8 | ||
|
|
dfa7178041 | ||
|
|
a3e9a3b686 | ||
|
|
599db049f2 | ||
|
|
7cba6dd690 | ||
|
|
2d406c9d58 | ||
|
|
d8fe2b10f1 | ||
|
|
c8343626ba | ||
|
|
5b188ec093 | ||
|
|
7ebbd80049 | ||
|
|
74c9e5d36d | ||
|
|
1a3530747f | ||
|
|
79899b9b18 | ||
|
|
6ef6a2f507 | ||
|
|
ab6d15c9f7 | ||
|
|
a0dc9139a2 | ||
|
|
220fa319d5 | ||
|
|
183d505a5e | ||
|
|
f960e7e736 | ||
|
|
d68ac84e03 | ||
|
|
30fbe2b6d7 | ||
|
|
e3a00570e1 | ||
|
|
499e02685a | ||
|
|
6b0418f772 | ||
|
|
1200aae47d | ||
|
|
20fdb09b33 | ||
|
|
522b12dc42 | ||
|
|
a7cd0dcce5 | ||
|
|
fd86c11336 | ||
|
|
4fc414d744 | ||
|
|
6d49e9b06c | ||
|
|
8b866e288b | ||
|
|
bf22588b0e | ||
|
|
60d5ca2811 | ||
|
|
9e5dcdfa69 | ||
|
|
3cc39dd5ed | ||
|
|
6878060346 | ||
|
|
9840061137 | ||
|
|
6f84e00463 | ||
|
|
1e82d0d34e | ||
|
|
f35bc468b3 | ||
|
|
719071c286 | ||
|
|
c4b439e2e6 | ||
|
|
77c2b56ceb | ||
|
|
0b17b7ad5a | ||
|
|
db3011a1c9 | ||
|
|
b47c695b97 | ||
|
|
55c1c893ba | ||
|
|
bde684fe30 | ||
|
|
89b6c9004b | ||
|
|
97cdfbddaf | ||
|
|
584ee2d136 | ||
|
|
21771c4725 | ||
|
|
7e8f1bed15 | ||
|
|
226ae03603 | ||
|
|
96c494f5ee | ||
|
|
23ba00422f | ||
|
|
52675c9c68 | ||
|
|
1b113e0114 | ||
|
|
0a930f5eeb | ||
|
|
84f5a83648 | ||
|
|
5fa277c254 | ||
|
|
d49c7f2dd4 | ||
|
|
a78f1688d9 | ||
|
|
cd37a846d8 | ||
|
|
63374ccb6d | ||
|
|
efe2d91fb3 | ||
|
|
234b2a019b | ||
|
|
4bbcd366bc | ||
|
|
5724f8747a | ||
|
|
38d44103d1 | ||
|
|
96a7ff68e7 | ||
|
|
fb78a55f76 | ||
|
|
fdc7fc7bcf | ||
|
|
ed5643aaf1 | ||
|
|
ac431ef9ef | ||
|
|
0940508637 | ||
|
|
704af7d04c | ||
|
|
e4e476a834 | ||
|
|
ece35b3c6f | ||
|
|
b26eab50b3 | ||
|
|
b42313a285 | ||
|
|
129e2c1668 | ||
|
|
87ddb81562 | ||
|
|
daee49cd9d | ||
|
|
3aaf773b0a | ||
|
|
460478fc65 | ||
|
|
fe8b5f45b9 | ||
|
|
01ac646c8e | ||
|
|
d4b9fbcc60 | ||
|
|
1cfa279543 | ||
|
|
e35be73bcc | ||
|
|
f8a5c46e18 | ||
|
|
1c7d85ce76 | ||
|
|
19004b4c36 | ||
|
|
930fa689a7 | ||
|
|
18a7b2d6b4 | ||
|
|
d2e03ff9d3 | ||
|
|
77a9c8395e | ||
|
|
c4151fcb95 | ||
|
|
4c5c21bba2 | ||
|
|
f0f6f1b71c | ||
|
|
a9d6d5a1db | ||
|
|
7849ce6de7 | ||
|
|
354bdcdbfb | ||
|
|
8ecd3e88c8 | ||
|
|
85d2b627f1 | ||
|
|
88fd92e4dc | ||
|
|
952982e7ea | ||
|
|
22e7c27af7 | ||
|
|
59c18c34de | ||
|
|
6acbaede44 | ||
|
|
1a44f875b3 | ||
|
|
9d935fe1d0 | ||
|
|
e5eed5b9b7 | ||
|
|
05a549f04f | ||
|
|
a5e68a4fae | ||
|
|
016a9e342a | ||
|
|
627d8950ae | ||
|
|
770ce08c10 | ||
|
|
69ac514b3b | ||
|
|
b1481748f9 | ||
|
|
49944e323f | ||
|
|
15310db389 | ||
|
|
ea5c5a2604 | ||
|
|
e461dd1355 | ||
|
|
e299c3e9d8 | ||
|
|
406c47271f | ||
|
|
05c7efea85 | ||
|
|
ce7ddf7055 | ||
|
|
beb40b5db6 | ||
|
|
2def752cc4 | ||
|
|
bacf3ae86a | ||
|
|
0fef382f2e | ||
|
|
95523d8538 | ||
|
|
71f7220bfd | ||
|
|
2212c6deac | ||
|
|
fb3efe4cfd | ||
|
|
e66ac6a7d0 | ||
|
|
7ab3908848 | ||
|
|
921f1fbfe8 | ||
|
|
2ac455f8b5 | ||
|
|
1ce881aed2 | ||
|
|
b1b5140951 | ||
|
|
b109c23233 | ||
|
|
a7a34a0b6e | ||
|
|
4bf63934e1 | ||
|
|
16f572282f | ||
|
|
44380c3700 | ||
|
|
dc46cb1386 | ||
|
|
3ccb1e8ad7 | ||
|
|
d973c5f48b | ||
|
|
f4af78c834 | ||
|
|
e6d323fd30 | ||
|
|
e6ab56aeb5 | ||
|
|
779765b649 | ||
|
|
6da730779a | ||
|
|
a3e77edc57 | ||
|
|
ed00308986 | ||
|
|
89e9092e0f | ||
|
|
f8b11754c8 | ||
|
|
4b38d0793c | ||
|
|
b2156f8154 | ||
|
|
3a5422e635 | ||
|
|
54d3d76868 | ||
|
|
f4dc0ec1b7 | ||
|
|
f500db2dd3 | ||
|
|
95f64f9934 | ||
|
|
cccb0e1a21 | ||
|
|
b434a4227f | ||
|
|
6ba4dc1f04 | ||
|
|
2fe4c81d1e | ||
|
|
5c00264184 | ||
|
|
c744849c9b | ||
|
|
f59b278f00 | ||
|
|
b26c155d5f | ||
|
|
6da79b8745 | ||
|
|
0b92591b17 | ||
|
|
974456db54 | ||
|
|
a1326a80fe | ||
|
|
00d6946b24 | ||
|
|
c4ffde93c0 | ||
|
|
37bfe967ea | ||
|
|
9abbbfd6fb | ||
|
|
155cd08e39 | ||
|
|
e2e6bdf3bd | ||
|
|
810c42c743 | ||
|
|
99e4c1301e | ||
|
|
8c86a831fc | ||
|
|
5e976416a4 | ||
|
|
0339e14260 | ||
|
|
4b94fcebf1 | ||
|
|
ddd2a79f37 | ||
|
|
01a8f2dab3 | ||
|
|
801629d2c1 | ||
|
|
87d62c941f | ||
|
|
7e6e0fdcc5 | ||
|
|
a73b07424c | ||
|
|
0f9b983132 | ||
|
|
43e25902d3 | ||
|
|
2c27c8517f | ||
|
|
b496058a0e | ||
|
|
4313663bd1 | ||
|
|
dbdbfbd07a | ||
|
|
184b23d61f | ||
|
|
5c03b4f668 | ||
|
|
bdbe777d68 | ||
|
|
a838a18647 | ||
|
|
dd8dba63ea | ||
|
|
3f5a664ee7 | ||
|
|
707292e1ff | ||
|
|
9a81b63943 | ||
|
|
30216b7b80 | ||
|
|
b2fc91c2ce | ||
|
|
ef0328833c | ||
|
|
6a93f17a4a | ||
|
|
01bd07ac66 | ||
|
|
88859cfeca | ||
|
|
dfe563e2bc | ||
|
|
7fc0ff981d | ||
|
|
1a9132102d | ||
|
|
d39638282f | ||
|
|
219071654d | ||
|
|
7c415db7bd | ||
|
|
4354c340fc | ||
|
|
a4b484fa36 | ||
|
|
3757d12dc4 | ||
|
|
c3a97b29a9 | ||
|
|
b65e30ec70 | ||
|
|
23a1e0266a | ||
|
|
76acecfe50 | ||
|
|
5031c77afb | ||
|
|
af90b8c989 | ||
|
|
d06b4adad0 | ||
|
|
b961cde946 | ||
|
|
8cbbe2f312 | ||
|
|
c15a49d82d | ||
|
|
fc5b670c73 | ||
|
|
c8adcc4c47 | ||
|
|
41a755fe41 | ||
|
|
8def1bb29e | ||
|
|
d379b04e33 | ||
|
|
17a30e054e | ||
|
|
93809911de | ||
|
|
edeb2ca9f4 | ||
|
|
01662fc3b8 | ||
|
|
7d8f4b4c00 | ||
|
|
e2a3896bf0 | ||
|
|
446de8e0ff | ||
|
|
5ae6c95878 | ||
|
|
7cde349a50 | ||
|
|
61e640f574 | ||
|
|
ed122d9d8e | ||
|
|
34817f4536 | ||
|
|
134d2f0fda | ||
|
|
142973827c | ||
|
|
0998a0eabf | ||
|
|
a96108478b | ||
|
|
47444888c3 | ||
|
|
a4769058f4 | ||
|
|
a4cf4c40d4 | ||
|
|
934fe4d29b | ||
|
|
408012f2e5 | ||
|
|
d0078b830e | ||
|
|
f7f091e18c | ||
|
|
a969430247 | ||
|
|
e52948b2b7 | ||
|
|
53bb1b230b | ||
|
|
54e83aeaaa | ||
|
|
aa3129cab5 | ||
|
|
90520dddd7 | ||
|
|
03eb77070a | ||
|
|
4ba5c255b6 | ||
|
|
01817db873 | ||
|
|
46fcbd6c01 | ||
|
|
aa3e3de09e | ||
|
|
af3d48764d | ||
|
|
091f36b736 | ||
|
|
7107f79f42 | ||
|
|
9922db2336 | ||
|
|
75db570198 | ||
|
|
28a09f377b | ||
|
|
fd2e0855bb | ||
|
|
82e1d57bd6 | ||
|
|
a2fbb0b0c8 | ||
|
|
8feddf9932 | ||
|
|
feed34b1cf | ||
|
|
662c980cf2 | ||
|
|
f5ae530890 | ||
|
|
46bf7dd3ce | ||
|
|
5d4eb38204 | ||
|
|
66da658075 | ||
|
|
3477b74573 | ||
|
|
f3de4906b7 | ||
|
|
caded3f189 | ||
|
|
5196395495 | ||
|
|
8089a7ed9f | ||
|
|
99230d31d2 | ||
|
|
94bca03f59 | ||
|
|
49719b6e6d | ||
|
|
1bdb781452 | ||
|
|
c336f69a6b | ||
|
|
c8cb1ce208 | ||
|
|
814a6a80cd | ||
|
|
5fdfe18b32 | ||
|
|
7b7a74778b | ||
|
|
39dbd46556 | ||
|
|
1db4a14be4 | ||
|
|
4a4ea4e196 | ||
|
|
e0724441eb | ||
|
|
5d47895515 | ||
|
|
c1dfac7260 | ||
|
|
bf29cb3bae | ||
|
|
a0a9b3f851 | ||
|
|
4c4deb22c9 | ||
|
|
a42c497055 | ||
|
|
f1dcdb20bc | ||
|
|
46330ae201 | ||
|
|
bfe3595b4c | ||
|
|
34c39e6a55 | ||
|
|
5a85501919 | ||
|
|
97a4282e5e | ||
|
|
39c13b50a3 | ||
|
|
ad304e321b | ||
|
|
8c0b2da461 | ||
|
|
72fce45b2b | ||
|
|
1f49d7fda6 | ||
|
|
eec8ee7027 | ||
|
|
188eb2e1e3 | ||
|
|
62867b32d9 | ||
|
|
ccebd2447d | ||
|
|
08dca75789 | ||
|
|
16b3e1381b | ||
|
|
f1cd639a09 | ||
|
|
be18e4de14 | ||
|
|
7e62c91d44 | ||
|
|
b2d5a103b5 | ||
|
|
4ee2cad39e | ||
|
|
b7c8a0038b | ||
|
|
8c27e8c379 | ||
|
|
0133aa47ff | ||
|
|
5659c925a2 | ||
|
|
27779ac792 | ||
|
|
3f1bfa4629 | ||
|
|
15a693c3ed | ||
|
|
b1d620e145 | ||
|
|
478fbd0aa9 | ||
|
|
ee906b7351 | ||
|
|
dd15f21ccb | ||
|
|
d7cd5fda7c | ||
|
|
174300b00f | ||
|
|
b2c8d8c855 | ||
|
|
2bad2b6bfe | ||
|
|
880d0ff855 | ||
|
|
e66cbee6cd | ||
|
|
03e470721e | ||
|
|
ecf73bcfa7 | ||
|
|
2c3a500286 | ||
|
|
8b83061cf4 | ||
|
|
e75c3207d6 | ||
|
|
41d4b5ba0b | ||
|
|
21fa1b168b | ||
|
|
91e5e7f2ab | ||
|
|
e3f7e2f1bd | ||
|
|
084cf80c60 | ||
|
|
632e3bbb08 | ||
|
|
17d17833b2 | ||
|
|
8e22bd9c1e | ||
|
|
98213743f3 | ||
|
|
bb855ed83d | ||
|
|
a8ef49e228 | ||
|
|
e0ad32dbd2 | ||
|
|
62bf769cad | ||
|
|
7488ff25b2 | ||
|
|
b69c9da983 | ||
|
|
d30fdef8aa | ||
|
|
9c5a6b9833 | ||
|
|
d300d265c4 | ||
|
|
1d72ce587f | ||
|
|
3fdb41dcb9 | ||
|
|
f20de2f04a | ||
|
|
31b31f111b | ||
|
|
2ae9fb9778 | ||
|
|
cd0da0f6bf | ||
|
|
cd9bfbb9fa | ||
|
|
ed0428bf97 | ||
|
|
c038a02051 | ||
|
|
31abcfeef4 | ||
|
|
5f32d9ccf5 | ||
|
|
0510600104 | ||
|
|
7f30fbf3c5 | ||
|
|
3d56260ca4 | ||
|
|
1137775da9 | ||
|
|
3951fdc938 | ||
|
|
5779e357dd | ||
|
|
2842d80f26 | ||
|
|
96387d8023 | ||
|
|
6720c19233 | ||
|
|
ef732b4700 | ||
|
|
ee7e3ee5a7 | ||
|
|
ceeed88fa5 | ||
|
|
79353a1d97 | ||
|
|
7fdc42c62f | ||
|
|
3a2e854a88 | ||
|
|
661a2d023a |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
24
.changeset/config.json
Normal file
24
.changeset/config.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [
|
||||
[
|
||||
"cojson",
|
||||
"jazz-tools",
|
||||
"jazz-browser",
|
||||
"jazz-browser-media-images",
|
||||
"jazz-react",
|
||||
"jazz-nodejs",
|
||||
"jazz-run",
|
||||
"cojson-transport-ws",
|
||||
"cojson-storage-indexeddb",
|
||||
"cojson-storage-sqlite"
|
||||
]
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
5
.changeset/curvy-geckos-prove.md
Normal file
5
.changeset/curvy-geckos-prove.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
Fix on CoMapInit to not allow null values on required refs
|
||||
5
.changeset/dry-crews-press.md
Normal file
5
.changeset/dry-crews-press.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Clean up binary stream ending logic
|
||||
5
.changeset/famous-shrimps-warn.md
Normal file
5
.changeset/famous-shrimps-warn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
fix: handle null values for co.refs
|
||||
5
.changeset/fluffy-deers-learn.md
Normal file
5
.changeset/fluffy-deers-learn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
Fix loadAsBlob resolving too early
|
||||
5
.changeset/forty-plants-kiss.md
Normal file
5
.changeset/forty-plants-kiss.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-run": patch
|
||||
---
|
||||
|
||||
Added sync command to start a local sync server
|
||||
5
.changeset/thin-olives-fold.md
Normal file
5
.changeset/thin-olives-fold.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-inspector": patch
|
||||
---
|
||||
|
||||
fix(inspector): subscribe to latent covalues instead of loading them immediately
|
||||
33
.github/workflows/build-and-deploy.yaml
vendored
33
.github/workflows/build-and-deploy.yaml
vendored
@@ -3,26 +3,28 @@ name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-examples:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["todo", "pets"]
|
||||
example: ["chat", "pets", "todo", "inspector"]
|
||||
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
@@ -34,14 +36,10 @@ jobs:
|
||||
username: gardencmp
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Nuke Workspace
|
||||
- name: Pnpm Build
|
||||
run: |
|
||||
rm package.json yarn.lock;
|
||||
|
||||
- name: Yarn Build
|
||||
run: |
|
||||
yarn install --frozen-lockfile;
|
||||
yarn build;
|
||||
pnpm install
|
||||
pnpm turbo build;
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
- name: Docker Build & Push
|
||||
@@ -53,12 +51,13 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
deploy-examples:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
needs: build-examples
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["todo", "pets"]
|
||||
example: ["chat", "pets", "todo", "inspector"]
|
||||
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -86,5 +85,5 @@ jobs:
|
||||
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
|
||||
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
50
.github/workflows/playwright.yml
vendored
Normal file
50
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["pets"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm Build
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm turbo build;
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm exec playwright test
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: ${{ matrix.example }}-playwright-report
|
||||
path: ./examples/${{ matrix.example }}/playwright-report/
|
||||
retention-days: 30
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
node_modules
|
||||
yarn-error.log
|
||||
lerna-debug.log
|
||||
docsTmp
|
||||
docsTmp
|
||||
.DS_Store
|
||||
.turbo
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
19
LICENSE.txt
Normal file
19
LICENSE.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2024, Garden Computing, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
84
README.md
84
README.md
@@ -1,82 +1,12 @@
|
||||
# Jazz - instant sync
|
||||
|
||||
Homepage: [jazz.tools](https://jazz.tools) — [Discord](https://discord.gg/utDMjHYg42)
|
||||
|
||||
Jazz is an open-source toolkit for *secure telepathic data.*
|
||||
|
||||
- Ship faster & simplify your frontend and backend
|
||||
- Get cross-device sync, real-time collaboration & offline support for free
|
||||
|
||||
[Jazz Global Mesh](https://jazz.tools/mesh) is serverless sync & storage for Jazz apps. (currently free!)
|
||||
# Jazz - Instant sync
|
||||
|
||||
|
||||
|
||||
## What is Secure Telepathic Data?
|
||||
**Jazz is an open-source toolkit for building apps with *distributed state.***
|
||||
|
||||
**Telepathic** means:
|
||||
- Homepage: [jazz.tools](https://jazz.tools)
|
||||
- Docs: [jazz.tools/docs](https://jazz.tools/docs)
|
||||
- Community & support: [Discord](https://discord.gg/utDMjHYg42)
|
||||
- Updates: [Twitter](https://twitter.com/jazz_tools) & [Email](https://gcmp.io/news)
|
||||
|
||||
- **Read and write data as if it was local,** from anywhere in your app.
|
||||
- **Always have that data synced, instantly.** Across devices of the same user — or to other users (coming soon: to your backend, workers, etc.)
|
||||
|
||||
**Secure** means:
|
||||
|
||||
- **Fine-grained, role-based permissions are *baked into* your data.**
|
||||
- **Permissions are enforced everywhere, locally.** (using cryptography instead of through an API)
|
||||
- Roles can be changed dynamically, supporting changing teams, invite links and more.
|
||||
|
||||
## How to build an app with Jazz?
|
||||
|
||||
### Building a new app, completely with Jazz
|
||||
|
||||
It's still a bit early, but these are the rough steps:
|
||||
|
||||
1. Define your data model with [CoJSON Values](#cojson).
|
||||
2. Implement permission logic using [CoJSON Groups](#group).
|
||||
3. Hook up a user interface with [jazz-react](#jazz-react).
|
||||
|
||||
The best example is currently the [Todo List app](#example-app-todo-list).
|
||||
|
||||
### Gradually adding Jazz to an existing app
|
||||
|
||||
Coming soon: Jazz will support gradual adoption by integrating with your existing UI, auth and database.
|
||||
|
||||
## Example App: Todo List
|
||||
|
||||
The best example of Jazz is currently the Todo List app.
|
||||
|
||||
- Live version: https://example-todo.jazz.tools
|
||||
- Source code: [`./examples/todo`](./examples/todo). See the README there for a walk-through and running instructions.
|
||||
|
||||
# Documentation
|
||||
|
||||
Note: Since it's early days, this is the only source of documentation so far.
|
||||
|
||||
If you want to build something with Jazz, [join the Jazz Discord](https://discord.gg/utDMjHYg42) for encouragement and help!
|
||||
|
||||
## Overview: Main Packages
|
||||
|
||||
**`cojson`** → [DOCS](./DOCS.md#cojson)
|
||||
|
||||
A library implementing abstractions and protocols for "Collaborative JSON". This will soon be standardized and forms the basis of secure telepathic data.
|
||||
|
||||
**`jazz-react`** → [DOCS](./DOCS.md#jazz-react)
|
||||
|
||||
Provides you with everything you need to build react apps around CoJSON, including reactive hooks for telepathic data, local IndexedDB persistence, support for different auth providers and helpers for simple invite links for CoJSON groups.
|
||||
|
||||
### Supporting packages
|
||||
<small>
|
||||
|
||||
**`cojson-simple-sync`**
|
||||
|
||||
A generic CoJSON sync server you can run locally if you don't want to use Jazz Global Mesh (the default sync backend, at `wss://sync.jazz.tools`)
|
||||
|
||||
**`jazz-browser`** → [DOCS](./DOCS.md#jazz-browser)
|
||||
|
||||
framework-agnostic primitives that allow you to use CoJSON in the browser. Used to implement `jazz-react`, will be used to implement bindings for other frameworks in the future.
|
||||
|
||||
**`jazz-react-auth-local`** (and `jazz-browser-auth-local`): A simple auth provider that stores cryptographic keys on user devices using WebAuthentication/Passkeys. Lets you build Jazz apps completely without a backend, with end-to-end encryption by default.
|
||||
|
||||
**`jazz-storage-indexeddb`**
|
||||
|
||||
Provides local, offline-capable persistence. Included and enabled in `jazz-react` by default.
|
||||
</small>
|
||||
Copyright 2024 — Garden Computing, Inc.
|
||||
13
examples/chat/.eslintrc.cjs
Normal file
13
examples/chat/.eslintrc.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {},
|
||||
}
|
||||
26
examples/chat/.gitignore
vendored
Normal file
26
examples/chat/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
sync-db/
|
||||
840
examples/chat/CHANGELOG.md
Normal file
840
examples/chat/CHANGELOG.md
Normal file
@@ -0,0 +1,840 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5d91f9f]
|
||||
- Updated dependencies [5094e6d]
|
||||
- Updated dependencies [b09589b]
|
||||
- Updated dependencies [2c3a40c]
|
||||
- Updated dependencies [4e16575]
|
||||
- Updated dependencies [ea882ab]
|
||||
- cojson@0.7.34
|
||||
- jazz-react@0.7.34
|
||||
- jazz-tools@0.7.34
|
||||
|
||||
## 0.0.81-neverthrow.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.8
|
||||
- jazz-react@0.7.34-neverthrow.8
|
||||
- jazz-tools@0.7.34-neverthrow.8
|
||||
|
||||
## 0.0.81-neverthrow.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.7
|
||||
- jazz-react@0.7.34-neverthrow.7
|
||||
- jazz-tools@0.7.34-neverthrow.7
|
||||
|
||||
## 0.0.81-neverthrow.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.4
|
||||
- jazz-react@0.7.34-neverthrow.4
|
||||
- jazz-tools@0.7.34-neverthrow.4
|
||||
|
||||
## 0.0.81-neverthrow.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.3
|
||||
- jazz-react@0.7.34-neverthrow.3
|
||||
- jazz-tools@0.7.34-neverthrow.3
|
||||
|
||||
## 0.0.81-neverthrow.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.34-neverthrow.2
|
||||
|
||||
## 0.0.81-neverthrow.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.1
|
||||
- jazz-react@0.7.34-neverthrow.1
|
||||
- jazz-tools@0.7.34-neverthrow.1
|
||||
|
||||
## 0.0.81-neverthrow.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.0
|
||||
- jazz-react@0.7.34-neverthrow.0
|
||||
- jazz-tools@0.7.34-neverthrow.0
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [b297c93]
|
||||
- Updated dependencies [3bf5127]
|
||||
- Updated dependencies [a8b74ff]
|
||||
- Updated dependencies [db53161]
|
||||
- cojson@0.7.33
|
||||
- jazz-react@0.7.33
|
||||
- jazz-tools@0.7.33
|
||||
|
||||
## 0.0.80-hotfixes.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.5
|
||||
- jazz-react@0.7.33-hotfixes.5
|
||||
- jazz-tools@0.7.33-hotfixes.5
|
||||
|
||||
## 0.0.80-hotfixes.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.4
|
||||
- jazz-react@0.7.33-hotfixes.4
|
||||
- jazz-tools@0.7.33-hotfixes.4
|
||||
|
||||
## 0.0.80-hotfixes.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.3
|
||||
- jazz-react@0.7.33-hotfixes.3
|
||||
- jazz-tools@0.7.33-hotfixes.3
|
||||
|
||||
## 0.0.80-hotfixes.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.33-hotfixes.2
|
||||
|
||||
## 0.0.80-hotfixes.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.33-hotfixes.1
|
||||
|
||||
## 0.0.80-hotfixes.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.0
|
||||
- jazz-react@0.7.33-hotfixes.0
|
||||
- jazz-tools@0.7.33-hotfixes.0
|
||||
|
||||
## 0.0.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.32
|
||||
- jazz-react@0.7.32
|
||||
|
||||
## 0.0.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.31
|
||||
- jazz-react@0.7.31
|
||||
- jazz-tools@0.7.31
|
||||
|
||||
## 0.0.77
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.30
|
||||
|
||||
## 0.0.76
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.29
|
||||
- jazz-react@0.7.29
|
||||
- jazz-tools@0.7.29
|
||||
|
||||
## 0.0.75
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.28
|
||||
- jazz-react@0.7.28
|
||||
- jazz-tools@0.7.28
|
||||
|
||||
## 0.0.74
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.27
|
||||
|
||||
## 0.0.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.26
|
||||
- jazz-react@0.7.26
|
||||
- jazz-tools@0.7.26
|
||||
|
||||
## 0.0.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
- jazz-react@0.7.25
|
||||
|
||||
## 0.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
- jazz-react@0.7.24
|
||||
|
||||
## 0.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- jazz-react@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
|
||||
## 0.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.22
|
||||
|
||||
## 0.0.68
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
- jazz-react@0.7.21
|
||||
|
||||
## 0.0.67
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
- jazz-react@0.7.20
|
||||
|
||||
## 0.0.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
- jazz-react@0.7.19
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.18
|
||||
- jazz-react@0.7.18
|
||||
- jazz-tools@0.7.18
|
||||
|
||||
## 0.0.64
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.17
|
||||
- jazz-react@0.7.17
|
||||
- jazz-tools@0.7.17
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.16
|
||||
- jazz-react@0.7.16
|
||||
|
||||
## 0.0.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.15
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.14
|
||||
- jazz-tools@0.7.14
|
||||
- jazz-react@0.7.14
|
||||
|
||||
## 0.0.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.13
|
||||
- jazz-react@0.7.13
|
||||
|
||||
## 0.0.59
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.12
|
||||
- jazz-react@0.7.12
|
||||
|
||||
## 0.0.58
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.11
|
||||
- jazz-react@0.7.11
|
||||
- jazz-tools@0.7.11
|
||||
|
||||
## 0.0.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.10
|
||||
- jazz-react@0.7.10
|
||||
- jazz-tools@0.7.10
|
||||
|
||||
## 0.0.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.9
|
||||
- jazz-react@0.7.9
|
||||
- jazz-tools@0.7.9
|
||||
|
||||
## 0.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.8
|
||||
- jazz-react@0.7.8
|
||||
|
||||
## 0.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9fdc91c]
|
||||
- jazz-react@0.7.7
|
||||
|
||||
## 0.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.6
|
||||
- jazz-react@0.7.6
|
||||
|
||||
## 0.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.5
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.4
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.3
|
||||
- jazz-react@0.7.3
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.2
|
||||
|
||||
## 0.0.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.1
|
||||
- jazz-react@0.7.1
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8636319]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [8636319]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [96c494f]
|
||||
- Updated dependencies [59c18c3]
|
||||
- Updated dependencies [19f52b7]
|
||||
- Updated dependencies [8636319]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [d8fe2b1]
|
||||
- Updated dependencies [19004b4]
|
||||
- Updated dependencies [a78f168]
|
||||
- Updated dependencies [1200aae]
|
||||
- Updated dependencies [60d5ca2]
|
||||
- Updated dependencies [52675c9]
|
||||
- Updated dependencies [129e2c1]
|
||||
- Updated dependencies [6d49e9b]
|
||||
- Updated dependencies [1cfa279]
|
||||
- Updated dependencies [704af7d]
|
||||
- Updated dependencies [e97f730]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [460478f]
|
||||
- Updated dependencies [6b0418f]
|
||||
- Updated dependencies [e299c3e]
|
||||
- Updated dependencies [ed5643a]
|
||||
- Updated dependencies [bde684f]
|
||||
- Updated dependencies [bf0f8ec]
|
||||
- Updated dependencies [c4151fc]
|
||||
- Updated dependencies [63374cc]
|
||||
- Updated dependencies [8636319]
|
||||
- Updated dependencies [01ac646]
|
||||
- Updated dependencies [a5e68a4]
|
||||
- Updated dependencies [8636319]
|
||||
- Updated dependencies [952982e]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [5fa277c]
|
||||
- Updated dependencies [60d5ca2]
|
||||
- Updated dependencies [21771c4]
|
||||
- Updated dependencies [77c2b56]
|
||||
- Updated dependencies [63374cc]
|
||||
- Updated dependencies [d2e03ff]
|
||||
- Updated dependencies [354bdcd]
|
||||
- Updated dependencies [ece35b3]
|
||||
- Updated dependencies [60d5ca2]
|
||||
- Updated dependencies [69ac514]
|
||||
- Updated dependencies [f8a5c46]
|
||||
- Updated dependencies [f0f6f1b]
|
||||
- Updated dependencies [e5eed5b]
|
||||
- Updated dependencies [1a44f87]
|
||||
- Updated dependencies [627d895]
|
||||
- Updated dependencies [1200aae]
|
||||
- Updated dependencies [63374cc]
|
||||
- Updated dependencies [ece35b3]
|
||||
- Updated dependencies [38d4410]
|
||||
- Updated dependencies [85d2b62]
|
||||
- Updated dependencies [fd86c11]
|
||||
- Updated dependencies [52675c9]
|
||||
- jazz-tools@0.7.0
|
||||
- cojson@0.7.0
|
||||
- jazz-react@0.7.0
|
||||
- hash-slash@0.2.0
|
||||
|
||||
## 0.0.47-alpha.42
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.42
|
||||
- cojson@0.7.0-alpha.42
|
||||
- jazz-react@0.7.0-alpha.42
|
||||
|
||||
## 0.0.47-alpha.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.7.0-alpha.41
|
||||
- jazz-react@0.7.0-alpha.41
|
||||
|
||||
## 0.0.47-alpha.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.40
|
||||
|
||||
## 0.0.47-alpha.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.0-alpha.39
|
||||
- jazz-react@0.7.0-alpha.39
|
||||
- jazz-tools@0.7.0-alpha.39
|
||||
|
||||
## 0.0.47-alpha.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.38
|
||||
- jazz-react@0.7.0-alpha.38
|
||||
- cojson@0.7.0-alpha.38
|
||||
|
||||
## 0.0.47-alpha.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.37
|
||||
- cojson@0.7.0-alpha.37
|
||||
- jazz-tools@0.7.0-alpha.37
|
||||
|
||||
## 0.0.47-alpha.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [1a35307]
|
||||
- Updated dependencies [6b0418f]
|
||||
- Updated dependencies [1a35307]
|
||||
- cojson@0.7.0-alpha.36
|
||||
- jazz-tools@0.7.0-alpha.36
|
||||
- jazz-react@0.7.0-alpha.36
|
||||
|
||||
## 0.0.47-alpha.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- cojson@0.7.0-alpha.35
|
||||
- jazz-tools@0.7.0-alpha.35
|
||||
- jazz-react@0.7.0-alpha.35
|
||||
|
||||
## 0.0.47-alpha.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.34
|
||||
- jazz-react@0.7.0-alpha.34
|
||||
|
||||
## 0.0.47-alpha.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.33
|
||||
|
||||
## 0.0.47-alpha.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- hash-slash@0.2.0-alpha.3
|
||||
- jazz-tools@0.7.0-alpha.32
|
||||
- jazz-react@0.7.0-alpha.32
|
||||
|
||||
## 0.0.47-alpha.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.31
|
||||
- jazz-react@0.7.0-alpha.31
|
||||
|
||||
## 0.0.47-alpha.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.30
|
||||
- jazz-react@0.7.0-alpha.30
|
||||
|
||||
## 0.0.47-alpha.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.29
|
||||
- cojson@0.7.0-alpha.29
|
||||
- jazz-react@0.7.0-alpha.29
|
||||
|
||||
## 0.0.47-alpha.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.28
|
||||
- cojson@0.7.0-alpha.28
|
||||
- jazz-react@0.7.0-alpha.28
|
||||
|
||||
## 0.0.47-alpha.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.27
|
||||
- cojson@0.7.0-alpha.27
|
||||
- jazz-react@0.7.0-alpha.27
|
||||
|
||||
## 0.0.47-alpha.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.26
|
||||
- jazz-react@0.7.0-alpha.26
|
||||
|
||||
## 0.0.47-alpha.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.25
|
||||
- jazz-react@0.7.0-alpha.25
|
||||
|
||||
## 0.0.47-alpha.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.24
|
||||
- cojson@0.7.0-alpha.24
|
||||
- jazz-react@0.7.0-alpha.24
|
||||
|
||||
## 0.0.47-alpha.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.23
|
||||
- jazz-react@0.7.0-alpha.23
|
||||
|
||||
## 0.0.47-alpha.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.22
|
||||
- jazz-react@0.7.0-alpha.22
|
||||
|
||||
## 0.0.47-alpha.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.21
|
||||
- jazz-tools@0.7.0-alpha.21
|
||||
|
||||
## 0.0.47-alpha.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.20
|
||||
- jazz-tools@0.7.0-alpha.20
|
||||
|
||||
## 0.0.47-alpha.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.19
|
||||
- jazz-react@0.7.0-alpha.19
|
||||
|
||||
## 0.0.47-alpha.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.0-alpha.18
|
||||
|
||||
## 0.0.47-alpha.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.17
|
||||
- jazz-react@0.7.0-alpha.17
|
||||
|
||||
## 0.0.47-alpha.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.16
|
||||
- jazz-react@0.7.0-alpha.16
|
||||
|
||||
## 0.0.47-alpha.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.15
|
||||
- jazz-react@0.7.0-alpha.15
|
||||
|
||||
## 0.0.47-alpha.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.14
|
||||
- jazz-react@0.7.0-alpha.14
|
||||
|
||||
## 0.0.47-alpha.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.13
|
||||
- jazz-react@0.7.0-alpha.13
|
||||
|
||||
## 0.0.47-alpha.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.12
|
||||
- jazz-tools@0.7.0-alpha.12
|
||||
|
||||
## 0.0.47-alpha.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.11
|
||||
- jazz-tools@0.7.0-alpha.11
|
||||
- cojson@0.7.0-alpha.11
|
||||
|
||||
## 0.0.47-alpha.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.10
|
||||
- jazz-tools@0.7.0-alpha.10
|
||||
- cojson@0.7.0-alpha.10
|
||||
|
||||
## 0.0.47-alpha.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.9
|
||||
- jazz-tools@0.7.0-alpha.9
|
||||
|
||||
## 0.0.47-alpha.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.8
|
||||
- jazz-tools@0.7.0-alpha.8
|
||||
|
||||
## 0.0.47-alpha.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.7
|
||||
- jazz-tools@0.7.0-alpha.7
|
||||
- cojson@0.7.0-alpha.7
|
||||
|
||||
## 0.0.47-alpha.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.6
|
||||
- jazz-tools@0.7.0-alpha.6
|
||||
|
||||
## 0.0.47-alpha.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.0-alpha.5
|
||||
- jazz-tools@0.7.0-alpha.5
|
||||
- cojson@0.7.0-alpha.5
|
||||
|
||||
## 0.0.47-alpha.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.4
|
||||
- jazz-react@0.7.0-alpha.4
|
||||
|
||||
## 0.0.47-alpha.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.0-alpha.3
|
||||
- jazz-react@0.7.0-alpha.3
|
||||
|
||||
## 0.0.47-alpha.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- hash-slash@0.2.0-alpha.2
|
||||
- jazz-react@0.7.0-alpha.2
|
||||
- jazz-tools@0.7.0-alpha.2
|
||||
|
||||
## 0.0.47-alpha.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- hash-slash@0.2.0-alpha.1
|
||||
- jazz-react@0.7.0-alpha.1
|
||||
- jazz-tools@0.7.0-alpha.1
|
||||
- cojson@0.7.0-alpha.1
|
||||
|
||||
## 0.0.47-alpha.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- hash-slash@0.2.0-alpha.0
|
||||
- jazz-react@0.7.0-alpha.0
|
||||
- jazz-tools@0.7.0-alpha.0
|
||||
- cojson@0.7.0-alpha.0
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.5.0
|
||||
- jazz-react-auth-local@0.4.16
|
||||
4
examples/chat/Dockerfile
Normal file
4
examples/chat/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM caddy:2.7.3-alpine
|
||||
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||
|
||||
COPY ./dist /usr/share/caddy/
|
||||
42
examples/chat/README.md
Normal file
42
examples/chat/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Jazz Chat Example
|
||||
|
||||
Live version: [https://chat.jazz.tools](https://chat.jazz.tools)
|
||||
|
||||
## Installing & running the example locally
|
||||
|
||||
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
|
||||
|
||||
Start by checking out `jazz`
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gardencmp/jazz.git
|
||||
cd jazz/examples/chat
|
||||
pnpm pack --pack-destination /tmp
|
||||
mkdir -p ~/jazz-examples/chat # or any other directory
|
||||
tar -xf /tmp/jazz-example-chat-* --strip-components 1 -C ~/jazz-examples/chat
|
||||
cd ~/jazz-examples/chat
|
||||
```
|
||||
|
||||
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Start the dev server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Questions / problems / feedback
|
||||
|
||||
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||
|
||||
## Configuration: sync server
|
||||
|
||||
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||
|
||||
You can also run a local sync server by running `npx jazz-run sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).
|
||||
14
examples/chat/index.html
Normal file
14
examples/chat/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz Chat Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
56
examples/chat/job-template.nomad
Normal file
56
examples/chat/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
||||
job "chat$BRANCH_SUFFIX" {
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
to = 80
|
||||
}
|
||||
}
|
||||
|
||||
constraint {
|
||||
attribute = "${node.class}"
|
||||
operator = "="
|
||||
value = "mesh"
|
||||
}
|
||||
|
||||
spread {
|
||||
attribute = "${node.datacenter}"
|
||||
weight = 100
|
||||
}
|
||||
|
||||
constraint {
|
||||
distinct_hosts = true
|
||||
}
|
||||
|
||||
task "server" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "$DOCKER_TAG"
|
||||
ports = ["http"]
|
||||
|
||||
auth = {
|
||||
username = "$DOCKER_USER"
|
||||
password = "$DOCKER_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
name = "chat$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 50 # MHz
|
||||
memory = 50 # MB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# deploy bump 4
|
||||
54
examples/chat/package.json
Normal file
54
examples/chat/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.81",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "echo 'chat example is codegolfed'",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --fix",
|
||||
"*.{js,jsx,mdx,json}": "prettier --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"hash-slash": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"jazz-react": "workspace:*",
|
||||
"cojson": "workspace:*",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-use": "^17.4.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uniqolor": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
6
examples/chat/postcss.config.js
Normal file
6
examples/chat/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
examples/chat/public/jazz-logo.png
Normal file
BIN
examples/chat/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
43
examples/chat/src/app.tsx
Normal file
43
examples/chat/src/app.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CoMap, CoList, co, Group, ID } from "jazz-tools";
|
||||
import { createJazzReactContext, DemoAuth } from "jazz-react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { useIframeHashRouter } from "hash-slash";
|
||||
import { ChatScreen } from "./chatScreen.tsx";
|
||||
import { StrictMode } from "react";
|
||||
|
||||
export class Message extends CoMap {
|
||||
text = co.string;
|
||||
}
|
||||
|
||||
export class Chat extends CoList.Of(co.ref(Message)) {}
|
||||
|
||||
const Jazz = createJazzReactContext({
|
||||
auth: DemoAuth({ appName: "Jazz Chat" }),
|
||||
peer: `wss://mesh.jazz.tools/?key=you@example.com`
|
||||
});
|
||||
export const { useAccount, useCoState } = Jazz;
|
||||
|
||||
|
||||
function App() {
|
||||
const { me, logOut } = useAccount();
|
||||
|
||||
const createChat = () => {
|
||||
const group = Group.create({ owner: me });
|
||||
group.addMember("everyone", "writer");
|
||||
const chat = Chat.create([], { owner: group });
|
||||
location.hash = "/chat/" + chat.id;
|
||||
};
|
||||
|
||||
return <div className="flex flex-col items-center justify-between w-screen h-screen p-2 dark:bg-black dark:text-white">
|
||||
<div className="rounded mb-5 px-2 py-1 text-sm self-end">
|
||||
{me.profile?.name} · <button onClick={logOut}>Log Out</button>
|
||||
</div>
|
||||
{useIframeHashRouter().route({
|
||||
'/': () => createChat() as never,
|
||||
'/chat/:id': (id) => <ChatScreen chatID={id as ID<Chat>} />
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!)
|
||||
.render(<StrictMode><Jazz.Provider><App/></Jazz.Provider></StrictMode>);
|
||||
42
examples/chat/src/chatScreen.tsx
Normal file
42
examples/chat/src/chatScreen.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ID } from 'jazz-tools';
|
||||
import { Chat, Message, useCoState } from './app.tsx';
|
||||
|
||||
export function ChatScreen(props: { chatID: ID<Chat> }) {
|
||||
const chat = useCoState(Chat, props.chatID, [{}]);
|
||||
|
||||
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
|
||||
{chat.length > 0
|
||||
? chat.map((msg) => <ChatBubble msg={msg} key={msg.id} />)
|
||||
: <div className='m-auto text-sm'>(Empty chat)</div>}
|
||||
<ChatInput onSubmit={(text) => {
|
||||
chat.push(
|
||||
Message.create({ text }, { owner: chat._owner })
|
||||
);
|
||||
}} />
|
||||
</div> : <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function ChatBubble(props: { msg: Message }) {
|
||||
const lastEdit = props.msg._edits.text;
|
||||
const align = lastEdit.by?.isMe ? 'items-end' : 'items-start';
|
||||
|
||||
return <div className={`${align} flex flex-col`}>
|
||||
<div className='rounded-xl bg-stone-100 dark:bg-stone-700 dark:text-white py-2 px-4 mt-2 min-w-[5rem]'>
|
||||
{ props.msg.text }
|
||||
</div>
|
||||
<div className='text-xs text-neutral-500 ml-2'>
|
||||
{ lastEdit.by?.profile?.name }{' '}
|
||||
{ lastEdit.madeAt?.toLocaleTimeString() }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ChatInput(props: { onSubmit: (text: string) => void }) {
|
||||
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white border-stone-300 dark:border-stone-700'
|
||||
placeholder='Type a message and press Enter'
|
||||
onKeyDown={({ key, currentTarget: input }) => {
|
||||
if (key !== 'Enter' || !input.value) return;
|
||||
props.onSubmit(input.value);
|
||||
input.value = '';
|
||||
}} />;
|
||||
}
|
||||
78
examples/chat/src/index.css
Normal file
78
examples/chat/src/index.css
Normal file
@@ -0,0 +1,78 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
1
examples/chat/src/vite-env.d.ts
vendored
Normal file
1
examples/chat/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
75
examples/chat/tailwind.config.js
Normal file
75
examples/chat/tailwind.config.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
29
examples/chat/tsconfig.json
Normal file
29
examples/chat/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
examples/chat/tsconfig.node.json
Normal file
10
examples/chat/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
examples/chat/vite.config.ts
Normal file
16
examples/chat/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: false
|
||||
}
|
||||
})
|
||||
18
examples/inspector/.eslintrc.cjs
Normal file
18
examples/inspector/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
26
examples/inspector/.gitignore
vendored
Normal file
26
examples/inspector/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
sync-db/
|
||||
224
examples/inspector/CHANGELOG.md
Normal file
224
examples/inspector/CHANGELOG.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.59
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5d91f9f]
|
||||
- Updated dependencies [5094e6d]
|
||||
- Updated dependencies [b09589b]
|
||||
- Updated dependencies [2c3a40c]
|
||||
- Updated dependencies [406ab9b]
|
||||
- Updated dependencies [4e16575]
|
||||
- Updated dependencies [ea882ab]
|
||||
- cojson@0.7.34
|
||||
- cojson-transport-ws@0.7.34
|
||||
|
||||
## 0.0.59-neverthrow.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.8
|
||||
- cojson-transport-ws@0.7.34-neverthrow.8
|
||||
|
||||
## 0.0.59-neverthrow.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.7
|
||||
- cojson-transport-ws@0.7.34-neverthrow.7
|
||||
|
||||
## 0.0.59-neverthrow.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.4
|
||||
- cojson-transport-ws@0.7.34-neverthrow.4
|
||||
|
||||
## 0.0.59-neverthrow.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.3
|
||||
- cojson-transport-ws@0.7.34-neverthrow.3
|
||||
|
||||
## 0.0.59-neverthrow.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.34-neverthrow.2
|
||||
|
||||
## 0.0.59-neverthrow.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.1
|
||||
- cojson-transport-ws@0.7.34-neverthrow.1
|
||||
|
||||
## 0.0.59-neverthrow.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.34-neverthrow.0
|
||||
- cojson-transport-ws@0.7.34-neverthrow.0
|
||||
|
||||
## 0.0.58
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fdde8db]
|
||||
- Updated dependencies [b297c93]
|
||||
- Updated dependencies [07fe2b9]
|
||||
- Updated dependencies [3bf5127]
|
||||
- Updated dependencies [a8b74ff]
|
||||
- Updated dependencies [db53161]
|
||||
- cojson-transport-ws@0.7.33
|
||||
- cojson@0.7.33
|
||||
|
||||
## 0.0.58-hotfixes.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.5
|
||||
- cojson-transport-ws@0.7.33-hotfixes.5
|
||||
|
||||
## 0.0.58-hotfixes.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.4
|
||||
- cojson-transport-ws@0.7.33-hotfixes.4
|
||||
|
||||
## 0.0.58-hotfixes.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.33-hotfixes.3
|
||||
- cojson@0.7.33-hotfixes.3
|
||||
|
||||
## 0.0.58-hotfixes.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.33-hotfixes.2
|
||||
|
||||
## 0.0.58-hotfixes.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.33-hotfixes.1
|
||||
|
||||
## 0.0.58-hotfixes.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.33-hotfixes.0
|
||||
- cojson-transport-ws@0.7.33-hotfixes.0
|
||||
|
||||
## 0.0.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.31
|
||||
- cojson@0.7.31
|
||||
|
||||
## 0.0.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.30
|
||||
|
||||
## 0.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.29
|
||||
- cojson-transport-ws@0.7.29
|
||||
|
||||
## 0.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.28
|
||||
- cojson-transport-ws@0.7.28
|
||||
|
||||
## 0.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.27
|
||||
|
||||
## 0.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.26
|
||||
- cojson-transport-ws@0.7.26
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- cojson-transport-ws@0.7.23
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.22
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.18
|
||||
- cojson-transport-ws@0.7.18
|
||||
|
||||
## 0.0.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.17
|
||||
- cojson-transport-ws@0.7.17
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.6.7
|
||||
- jazz-react@0.5.5
|
||||
- jazz-react-auth-local@0.4.18
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.5.0
|
||||
- jazz-react-auth-local@0.4.16
|
||||
4
examples/inspector/Dockerfile
Normal file
4
examples/inspector/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM caddy:2.7.3-alpine
|
||||
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||
|
||||
COPY ./dist /usr/share/caddy/
|
||||
7
examples/inspector/README.md
Normal file
7
examples/inspector/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Jazz Inspector
|
||||
|
||||
Live address: https://inspector.jazz.tools
|
||||
|
||||
Use this to visually inspect a Jazz account or other CoValue.
|
||||
|
||||
For now, you can get your account credentials from the `jazz-logged-in-secret` local-storage key from within your Jazz app.
|
||||
17
examples/inspector/index.html
Normal file
17
examples/inspector/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz Inspector</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
56
examples/inspector/job-template.nomad
Normal file
56
examples/inspector/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
||||
job "inspector$BRANCH_SUFFIX" {
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
to = 80
|
||||
}
|
||||
}
|
||||
|
||||
constraint {
|
||||
attribute = "${node.class}"
|
||||
operator = "="
|
||||
value = "mesh"
|
||||
}
|
||||
|
||||
spread {
|
||||
attribute = "${node.datacenter}"
|
||||
weight = 100
|
||||
}
|
||||
|
||||
constraint {
|
||||
distinct_hosts = true
|
||||
}
|
||||
|
||||
task "server" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "$DOCKER_TAG"
|
||||
ports = ["http"]
|
||||
|
||||
auth = {
|
||||
username = "$DOCKER_USER"
|
||||
password = "$DOCKER_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
name = "inspector$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 50 # MHz
|
||||
memory = 50 # MB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# deploy bump 4
|
||||
48
examples/inspector/package.json
Normal file
48
examples/inspector/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"private": true,
|
||||
"version": "0.0.59",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"hash-slash": "workspace:*",
|
||||
"cojson": "workspace:*",
|
||||
"cojson-transport-ws": "workspace:*",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-use": "^17.4.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uniqolor": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
6
examples/inspector/postcss.config.js
Normal file
6
examples/inspector/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
examples/inspector/public/jazz-logo.png
Normal file
BIN
examples/inspector/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
4
examples/inspector/src/app.tsx
Normal file
4
examples/inspector/src/app.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./viewer/new-app";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
92
examples/inspector/src/index.css
Normal file
92
examples/inspector/src/index.css
Normal file
@@ -0,0 +1,92 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateZ(400px) translateY(30px) scale(1.05);
|
||||
opacity: 0.4;
|
||||
}
|
||||
to {
|
||||
transform: translateZ(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
18
examples/inspector/src/link-icon.tsx
Normal file
18
examples/inspector/src/link-icon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
42
examples/inspector/src/viewer/breadcrumbs.tsx
Normal file
42
examples/inspector/src/viewer/breadcrumbs.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { PageInfo } from "./types";
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
path: PageInfo[];
|
||||
onBreadcrumbClick: (index: number) => void;
|
||||
}
|
||||
|
||||
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
||||
path,
|
||||
onBreadcrumbClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className="z-20 relative bg-indigo-400/10 backdrop-blur-sm rounded-lg inline-flex px-2 py-1 whitespace-pre transition-all items-center space-x-1 min-h-10">
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(-1)}
|
||||
className="flex items-center justify-center p-1 rounded-sm hover:bg-indigo-500/10 transition-colors"
|
||||
aria-label="Go to home"
|
||||
>
|
||||
<img src="jazz-logo.png" alt="Jazz Logo" className="size-5" />
|
||||
</button>
|
||||
{path.map((page, index) => {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block first:pl-1 last:pr-1"
|
||||
>
|
||||
{index === 0 ? null : (
|
||||
<span className="text-indigo-500/30">{" / "}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(index)}
|
||||
className="text-indigo-700 hover:underline"
|
||||
>
|
||||
{index === 0 ? page.name || "Root" : page.name}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
353
examples/inspector/src/viewer/co-stream-view.tsx
Normal file
353
examples/inspector/src/viewer/co-stream-view.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
CoID,
|
||||
LocalNode,
|
||||
RawBinaryCoStream,
|
||||
RawCoStream,
|
||||
RawCoValue,
|
||||
} from "cojson";
|
||||
import { JsonObject, JsonValue } from "cojson/src/jsonValue";
|
||||
import { PageInfo } from "./types";
|
||||
import { base64URLtoBytes } from "cojson/src/base64url";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowDownToLine } from "lucide-react";
|
||||
import {
|
||||
BinaryStreamItem,
|
||||
BinaryStreamStart,
|
||||
CoStreamItem,
|
||||
} from "cojson/src/coValues/coStream";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
// typeguard for BinaryStreamStart
|
||||
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
|
||||
return (
|
||||
typeof item === "object" &&
|
||||
item !== null &&
|
||||
"type" in item &&
|
||||
item.type === "start"
|
||||
);
|
||||
}
|
||||
|
||||
function detectCoStreamType(value: RawCoStream | RawBinaryCoStream) {
|
||||
const firstKey = Object.keys(value.items)[0];
|
||||
if (!firstKey)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
|
||||
const items = value.items[firstKey as never]?.map((v) => v.value);
|
||||
|
||||
if (!items)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
const firstItem = items[0];
|
||||
if (!firstItem)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
// This is a binary stream
|
||||
if (isBinaryStreamStart(firstItem)) {
|
||||
return {
|
||||
type: "binary",
|
||||
items: items as BinaryStreamItem[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "coStream",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk = false,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
onlyFirstChunk?: boolean;
|
||||
}) {
|
||||
if (onlyFirstChunk && items.length > 1) {
|
||||
items = items.slice(0, 2);
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
const binary_U_prefixLength = 8;
|
||||
|
||||
let lastProgressUpdate = Date.now();
|
||||
|
||||
for (const item of items.slice(1)) {
|
||||
if (item.type === "end") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.type !== "chunk") {
|
||||
console.error("Invalid binary stream chunk", item);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chunk = base64URLtoBytes(item.chunk.slice(binary_U_prefixLength));
|
||||
// totalLength += chunk.length;
|
||||
chunks.push(chunk);
|
||||
|
||||
if (Date.now() - lastProgressUpdate > 100) {
|
||||
lastProgressUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
const defaultMime = "mimeType" in items[0] ? items[0].mimeType : null;
|
||||
|
||||
const blob = new Blob(chunks, defaultMime ? { type: defaultMime } : {});
|
||||
|
||||
const mimeType =
|
||||
defaultMime === "" ? await detectPDFMimeType(blob) : defaultMime;
|
||||
|
||||
return {
|
||||
blob,
|
||||
mimeType: mimeType as string,
|
||||
unfinishedChunks: items.length > 1,
|
||||
totalSize:
|
||||
"totalSizeBytes" in items[0]
|
||||
? (items[0].totalSizeBytes as number)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const detectPDFMimeType = async (blob: Blob): Promise<string> => {
|
||||
const arrayBuffer = await blob.slice(0, 4).arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const header = uint8Array.reduce(
|
||||
(acc, byte) => acc + String.fromCharCode(byte),
|
||||
"",
|
||||
);
|
||||
|
||||
if (header === "%PDF") {
|
||||
return "application/pdf";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
};
|
||||
|
||||
const BinaryDownloadButton = ({
|
||||
pdfBlob,
|
||||
fileName = "document",
|
||||
label,
|
||||
mimeType,
|
||||
}: {
|
||||
pdfBlob: Blob;
|
||||
mimeType?: string;
|
||||
fileName?: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const downloadFile = () => {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([pdfBlob], mimeType ? { type: mimeType } : {}),
|
||||
);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download =
|
||||
mimeType === "application/pdf" ? `${fileName}.pdf` : fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 text-gray-900 border border-gray-900/10 bg-clip-border shadow-sm transition-colors rounded bg-gray-50 text-sm"
|
||||
onClick={downloadFile}
|
||||
>
|
||||
<ArrowDownToLine size={16} />
|
||||
{label}
|
||||
{/* Download {mimeType === "application/pdf" ? "PDF" : "File"} */}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelContentPair = ({
|
||||
label,
|
||||
content,
|
||||
}: {
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 ">
|
||||
<span className="uppercase text-xs font-medium text-gray-600 tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
<span>{content}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function RenderCoBinaryStream({
|
||||
value,
|
||||
items,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
value: RawBinaryCoStream;
|
||||
}) {
|
||||
const [file, setFile] = useState<
|
||||
| {
|
||||
blob: Blob;
|
||||
mimeType: string;
|
||||
unfinishedChunks: boolean;
|
||||
totalSize: number | undefined;
|
||||
}
|
||||
| undefined
|
||||
| null
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// load only the first chunk to get the mime type and size
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk: true,
|
||||
})
|
||||
.then((v) => {
|
||||
if (v) {
|
||||
setFile(v);
|
||||
if (v.mimeType.includes("image")) {
|
||||
// If it's an image, load the full blob
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
}).then((s) => {
|
||||
if (s) setFile(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [items]);
|
||||
|
||||
if (!isLoading && !file) return <div>No blob</div>;
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (!file) return <div>No blob</div>;
|
||||
|
||||
const { blob, mimeType } = file;
|
||||
|
||||
const sizeInKB = (file.totalSize || 0) / 1024;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 mt-4">
|
||||
<div className="grid grid-cols-3 gap-2 max-w-3xl">
|
||||
<LabelContentPair
|
||||
label="Mime Type"
|
||||
content={
|
||||
<span className="font-mono bg-gray-100 rounded px-2 py-1 text-sm">
|
||||
{mimeType || "No mime type"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Size"
|
||||
content={<span>{sizeInKB.toFixed(2)} KB</span>}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Download"
|
||||
content={
|
||||
<BinaryDownloadButton
|
||||
fileName={value.id.toString()}
|
||||
pdfBlob={blob}
|
||||
mimeType={mimeType}
|
||||
label={
|
||||
mimeType === "application/pdf"
|
||||
? "Download PDF"
|
||||
: "Download File"
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
|
||||
<LabelContentPair
|
||||
label="Preview"
|
||||
content={
|
||||
<div className="bg-gray-50 p-3 rounded-sm">
|
||||
<RenderBlobImage blob={blob} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCoStream({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
value: RawCoStream;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const streamPerUser = Object.keys(value.items);
|
||||
const userCoIds = streamPerUser.map(
|
||||
(stream) => stream.split("_session")[0],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{userCoIds.map((id, idx) => (
|
||||
<div
|
||||
className="bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
key={id}
|
||||
>
|
||||
<AccountOrGroupPreview
|
||||
coId={id as CoID<RawCoValue>}
|
||||
node={node}
|
||||
/>
|
||||
{/* @ts-expect-error - TODO: fix types */}
|
||||
{value.items[streamPerUser[idx]]?.map(
|
||||
(item: CoStreamItem<JsonValue>) => (
|
||||
<div>
|
||||
{new Date(item.madeAt).toLocaleString()}{" "}
|
||||
{JSON.stringify(item.value)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CoStreamView({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
value: RawCoStream;
|
||||
}) {
|
||||
// if (!value) return <div>No value</div>;
|
||||
|
||||
const streamType = detectCoStreamType(value);
|
||||
|
||||
if (streamType.type === "binary") {
|
||||
if (streamType.items === undefined) {
|
||||
return <div>No binary stream</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCoBinaryStream
|
||||
value={value as RawBinaryCoStream}
|
||||
items={streamType.items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (streamType.type === "coStream") {
|
||||
return <RenderCoStream value={value} node={node} />;
|
||||
}
|
||||
|
||||
if (streamType.type === "unknown") return <div>Unknown stream type</div>;
|
||||
|
||||
return <div>Unknown stream type</div>;
|
||||
}
|
||||
|
||||
function RenderBlobImage({ blob }: { blob: Blob }) {
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
return <img src={urlCreator.createObjectURL(blob)} />;
|
||||
}
|
||||
73
examples/inspector/src/viewer/grid-view.tsx
Normal file
73
examples/inspector/src/viewer/grid-view.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson/src/jsonValue";
|
||||
import { CoMapPreview, ValueRenderer } from "./value-renderer";
|
||||
import clsx from "clsx";
|
||||
import { PageInfo, isCoId } from "./types";
|
||||
import { ResolveIcon } from "./type-icon";
|
||||
|
||||
export function GridView({
|
||||
data,
|
||||
onNavigate,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const entries = Object.entries(data);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-2">
|
||||
{entries.map(([key, child], childIndex) => (
|
||||
<div
|
||||
key={childIndex}
|
||||
className={clsx(
|
||||
"bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden",
|
||||
isCoId(child)
|
||||
? "bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
: "bg-gray-50",
|
||||
)}
|
||||
onClick={() =>
|
||||
isCoId(child) &&
|
||||
onNavigate([
|
||||
{ coId: child as CoID<RawCoValue>, name: key },
|
||||
])
|
||||
}
|
||||
>
|
||||
<h3 className="truncate">
|
||||
{isCoId(child) ? (
|
||||
<span className="font-medium flex justify-between">
|
||||
{key}
|
||||
|
||||
<div className="px-2 py-1 text-xs bg-gray-100 rounded">
|
||||
<ResolveIcon
|
||||
coId={child as CoID<RawCoValue>}
|
||||
node={node}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span>{key}</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm">
|
||||
{isCoId(child) ? (
|
||||
<CoMapPreview
|
||||
coId={child as CoID<RawCoValue>}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<ValueRenderer
|
||||
json={child}
|
||||
onCoIDClick={(coId) => {
|
||||
onNavigate([{ coId, name: key }]);
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
examples/inspector/src/viewer/index.tsx
Normal file
27
examples/inspector/src/viewer/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { LocalNode } from "cojson";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
import { PageInfo } from "./types";
|
||||
import { PageStack } from "./page-stack";
|
||||
|
||||
export default function CoJsonViewer({
|
||||
defaultPath,
|
||||
node,
|
||||
}: {
|
||||
defaultPath?: PageInfo[];
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const { path, addPages, goToIndex, goBack } = usePagePath(defaultPath);
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden">
|
||||
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
||||
<PageStack
|
||||
path={path}
|
||||
node={node}
|
||||
goBack={goBack}
|
||||
addPages={addPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
310
examples/inspector/src/viewer/new-app.tsx
Normal file
310
examples/inspector/src/viewer/new-app.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
LocalNode,
|
||||
CoID,
|
||||
RawCoValue,
|
||||
RawAccount,
|
||||
AgentSecret,
|
||||
AccountID,
|
||||
cojsonInternals,
|
||||
WasmCrypto,
|
||||
} from "cojson";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
import { PageStack } from "./page-stack";
|
||||
import { resolveCoValue, useResolvedCoValue } from "./use-resolve-covalue";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Account {
|
||||
id: CoID<RawAccount>;
|
||||
secret: AgentSecret;
|
||||
}
|
||||
|
||||
export default function CoJsonViewerApp() {
|
||||
const [accounts, setAccounts] = useState<Account[]>(() => {
|
||||
const storedAccounts = localStorage.getItem("inspectorAccounts");
|
||||
return storedAccounts ? JSON.parse(storedAccounts) : [];
|
||||
});
|
||||
const [currentAccount, setCurrentAccount] = useState<Account | null>(() => {
|
||||
const lastSelectedId = localStorage.getItem("lastSelectedAccountId");
|
||||
if (lastSelectedId) {
|
||||
const lastAccount = accounts.find(
|
||||
(account) => account.id === lastSelectedId,
|
||||
);
|
||||
return lastAccount || null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const [localNode, setLocalNode] = useState<LocalNode | null>(null);
|
||||
const [coValueId, setCoValueId] = useState<CoID<RawCoValue> | "">("");
|
||||
const { path, addPages, goToIndex, goBack, setPage } = usePagePath();
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("inspectorAccounts", JSON.stringify(accounts));
|
||||
}, [accounts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentAccount) {
|
||||
localStorage.setItem("lastSelectedAccountId", currentAccount.id);
|
||||
} else {
|
||||
localStorage.removeItem("lastSelectedAccountId");
|
||||
}
|
||||
}, [currentAccount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentAccount) {
|
||||
setLocalNode(null);
|
||||
goToIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
WasmCrypto.create().then(async (crypto) => {
|
||||
const wsPeer = createWebSocketPeer({
|
||||
id: "mesh",
|
||||
websocket: new WebSocket("wss://mesh.jazz.tools"),
|
||||
role: "server",
|
||||
});
|
||||
const node = await LocalNode.withLoadedAccount({
|
||||
accountID: currentAccount.id,
|
||||
accountSecret: currentAccount.secret,
|
||||
sessionID: cojsonInternals.newRandomSessionID(
|
||||
currentAccount.id,
|
||||
),
|
||||
peersToLoadFrom: [wsPeer],
|
||||
crypto,
|
||||
migration: async () => {
|
||||
console.log("Not running any migration in inspector");
|
||||
},
|
||||
});
|
||||
setLocalNode(node);
|
||||
});
|
||||
}, [currentAccount, goToIndex]);
|
||||
|
||||
const addAccount = (id: AccountID, secret: AgentSecret) => {
|
||||
const newAccount = { id, secret };
|
||||
setAccounts([...accounts, newAccount]);
|
||||
setCurrentAccount(newAccount);
|
||||
};
|
||||
|
||||
const deleteCurrentAccount = () => {
|
||||
if (currentAccount) {
|
||||
const updatedAccounts = accounts.filter(
|
||||
(account) => account.id !== currentAccount.id,
|
||||
);
|
||||
setAccounts(updatedAccounts);
|
||||
setCurrentAccount(
|
||||
updatedAccounts.length > 0 ? updatedAccounts[0] : null,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCoValueIdSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (coValueId) {
|
||||
setPage(coValueId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden flex flex-col">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
||||
<AccountSwitcher
|
||||
accounts={accounts}
|
||||
currentAccount={currentAccount}
|
||||
setCurrentAccount={setCurrentAccount}
|
||||
deleteCurrentAccount={deleteCurrentAccount}
|
||||
localNode={localNode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PageStack
|
||||
path={path}
|
||||
node={localNode}
|
||||
goBack={goBack}
|
||||
addPages={addPages}
|
||||
>
|
||||
{!currentAccount ? (
|
||||
<AddAccountForm addAccount={addAccount} />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleCoValueIdSubmit}
|
||||
aria-hidden={path.length !== 0}
|
||||
className={clsx(
|
||||
"flex flex-col justify-center items-center gap-2 h-full w-full mb-20 ",
|
||||
"transition-all duration-150",
|
||||
path.length > 0
|
||||
? "opacity-0 -translate-y-2 scale-95"
|
||||
: "opacity-100",
|
||||
)}
|
||||
>
|
||||
<fieldset className="flex flex-col gap-2 text-sm">
|
||||
<h2 className="text-3xl font-medium text-gray-950 text-center mb-4">
|
||||
Jazz CoValue Inspector
|
||||
</h2>
|
||||
<input
|
||||
className="border p-4 rounded-lg min-w-[21rem] font-mono"
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(
|
||||
e.target.value as CoID<RawCoValue>,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 hover:bg-indigo-500/80 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Inspect
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
className="border inline-block px-2 py-1.5 text-black rounded"
|
||||
onClick={() => {
|
||||
setCoValueId(currentAccount.id);
|
||||
setPage(currentAccount.id);
|
||||
}}
|
||||
>
|
||||
Inspect My Account
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
)}
|
||||
</PageStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountSwitcher({
|
||||
accounts,
|
||||
currentAccount,
|
||||
setCurrentAccount,
|
||||
deleteCurrentAccount,
|
||||
localNode,
|
||||
}: {
|
||||
accounts: Account[];
|
||||
currentAccount: Account | null;
|
||||
setCurrentAccount: (account: Account | null) => void;
|
||||
deleteCurrentAccount: () => void;
|
||||
localNode: LocalNode | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
<select
|
||||
value={currentAccount?.id || "add-account"}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "add-account") {
|
||||
setCurrentAccount(null);
|
||||
} else {
|
||||
const account = accounts.find(
|
||||
(a) => a.id === e.target.value,
|
||||
);
|
||||
setCurrentAccount(account || null);
|
||||
}
|
||||
}}
|
||||
className="p-2 px-4 bg-gray-100/50 border border-indigo-500/10 backdrop-blur-sm rounded-md text-indigo-700 appearance-none"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{localNode ? (
|
||||
<AccountNameDisplay
|
||||
accountId={account.id}
|
||||
node={localNode}
|
||||
/>
|
||||
) : (
|
||||
account.id
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
<option value="add-account">Add account</option>
|
||||
</select>
|
||||
{currentAccount && (
|
||||
<button
|
||||
onClick={deleteCurrentAccount}
|
||||
className="p-3 rounded hover:bg-gray-200 transition-colors"
|
||||
title="Delete Account"
|
||||
>
|
||||
<Trash2 size={16} className="text-gray-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddAccountForm({
|
||||
addAccount,
|
||||
}: {
|
||||
addAccount: (id: AccountID, secret: AgentSecret) => void;
|
||||
}) {
|
||||
const [id, setId] = useState("");
|
||||
const [secret, setSecret] = useState("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
addAccount(id as AccountID, secret as AgentSecret);
|
||||
setId("");
|
||||
setSecret("");
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-2 max-w-md mx-auto h-full justify-center"
|
||||
>
|
||||
<h2 className="text-2xl font-medium text-gray-900 mb-3">
|
||||
Add an Account to Inspect
|
||||
</h2>
|
||||
<input
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account ID"
|
||||
value={id}
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account Secret"
|
||||
value={secret}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Add Account
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountNameDisplay({
|
||||
accountId,
|
||||
node,
|
||||
}: {
|
||||
accountId: CoID<RawAccount>;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const { snapshot } = useResolvedCoValue(accountId, node);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot && typeof snapshot === "object" && "profile" in snapshot) {
|
||||
const profileId = snapshot.profile as CoID<RawCoValue>;
|
||||
resolveCoValue(profileId, node).then((profileResult) => {
|
||||
if (
|
||||
profileResult.snapshot &&
|
||||
typeof profileResult.snapshot === "object" &&
|
||||
"name" in profileResult.snapshot
|
||||
) {
|
||||
setName(profileResult.snapshot.name as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [snapshot, node]);
|
||||
|
||||
return name ? `${name} <${accountId}>` : accountId;
|
||||
}
|
||||
55
examples/inspector/src/viewer/page-stack.tsx
Normal file
55
examples/inspector/src/viewer/page-stack.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Page } from "./page"; // Assuming you have a Page component
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
|
||||
// Define the structure of a page in the path
|
||||
interface PageInfo {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Props for the PageStack component
|
||||
interface PageStackProps {
|
||||
path: PageInfo[];
|
||||
node?: LocalNode | null;
|
||||
goBack: () => void;
|
||||
addPages: (pages: PageInfo[]) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageStack({
|
||||
path,
|
||||
node,
|
||||
goBack,
|
||||
addPages,
|
||||
children,
|
||||
}: PageStackProps) {
|
||||
return (
|
||||
<div className="relative mt-4 h-[calc(100vh-6rem)]">
|
||||
{children && (
|
||||
<div className="absolute inset-0 pb-20">{children}</div>
|
||||
)}
|
||||
{node &&
|
||||
path.map((page, index) => (
|
||||
<Page
|
||||
key={`${page.coId}-${index}`}
|
||||
coId={page.coId}
|
||||
node={node}
|
||||
name={page.name || page.coId}
|
||||
onHeaderClick={goBack}
|
||||
onNavigate={addPages}
|
||||
isTopLevel={index === path.length - 1}
|
||||
style={{
|
||||
transform: `translateZ(${(index - path.length + 1) * 200}px) scale(${
|
||||
1 - (path.length - index - 1) * 0.05
|
||||
}) translateY(${-(index - path.length + 1) * -4}%)`,
|
||||
opacity: 1 - (path.length - index - 1) * 0.05,
|
||||
zIndex: index,
|
||||
transitionProperty: "transform, opacity",
|
||||
transitionDuration: "0.3s",
|
||||
transitionTimingFunction: "ease-out",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
examples/inspector/src/viewer/page.tsx
Normal file
154
examples/inspector/src/viewer/page.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useResolvedCoValue } from "./use-resolve-covalue";
|
||||
import { GridView } from "./grid-view";
|
||||
import { PageInfo } from "./types";
|
||||
import { TableView } from "./table-viewer";
|
||||
import { TypeIcon } from "./type-icon";
|
||||
import { CoStreamView } from "./co-stream-view";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
type PageProps = {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
name: string;
|
||||
onNavigate: (newPages: PageInfo[]) => void;
|
||||
onHeaderClick?: () => void;
|
||||
isTopLevel?: boolean;
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function Page({
|
||||
coId,
|
||||
node,
|
||||
name,
|
||||
onNavigate,
|
||||
onHeaderClick,
|
||||
style,
|
||||
isTopLevel,
|
||||
}: PageProps) {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||||
|
||||
const supportsTableView = type === "colist" || extendedType === "record";
|
||||
|
||||
// Automatically switch to table view if the page is a CoMap record
|
||||
useEffect(() => {
|
||||
if (supportsTableView) {
|
||||
setViewMode("table");
|
||||
}
|
||||
}, [supportsTableView]);
|
||||
|
||||
if (snapshot === "unavailable") {
|
||||
return <div style={style}>Data unavailable</div>;
|
||||
}
|
||||
|
||||
if (!snapshot) {
|
||||
return <div style={style}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(
|
||||
"absolute inset-0 border border-gray-900/5 bg-clip-padding bg-white rounded-xl shadow-lg p-6 animate-in",
|
||||
)}
|
||||
>
|
||||
{!isTopLevel && (
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-10"
|
||||
aria-label="Back"
|
||||
onClick={() => {
|
||||
onHeaderClick?.();
|
||||
}}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-bold flex items-start flex-col gap-1">
|
||||
<span>
|
||||
{name}
|
||||
{typeof snapshot === "object" &&
|
||||
"name" in snapshot ? (
|
||||
<span className="text-gray-600 font-medium">
|
||||
{" "}
|
||||
{
|
||||
(
|
||||
snapshot as {
|
||||
name: string;
|
||||
}
|
||||
).name
|
||||
}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{type && (
|
||||
<TypeIcon
|
||||
type={type}
|
||||
extendedType={extendedType}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{coId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* {supportsTableView && (
|
||||
<button
|
||||
onClick={toggleViewMode}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
{viewMode === "grid" ? "Table View" : "Grid View"}
|
||||
</button>
|
||||
)} */}
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[calc(100%-4rem)]">
|
||||
{type === "costream" ? (
|
||||
<CoStreamView
|
||||
data={snapshot}
|
||||
onNavigate={onNavigate}
|
||||
node={node}
|
||||
value={value as RawCoStream}
|
||||
/>
|
||||
) : viewMode === "grid" ? (
|
||||
<GridView
|
||||
data={snapshot}
|
||||
onNavigate={onNavigate}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<TableView
|
||||
data={snapshot}
|
||||
node={node}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
{/* --- */}
|
||||
{extendedType !== "account" && extendedType !== "group" && (
|
||||
<div className="text-xs text-gray-500 mt-4">
|
||||
Owned by{" "}
|
||||
<AccountOrGroupPreview
|
||||
coId={value.group.id}
|
||||
node={node}
|
||||
showId
|
||||
onClick={() => {
|
||||
onNavigate([
|
||||
{ coId: value.group.id, name: "owner" },
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
examples/inspector/src/viewer/table-viewer.tsx
Normal file
142
examples/inspector/src/viewer/table-viewer.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson/src/jsonValue";
|
||||
import { PageInfo } from "./types";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ValueRenderer } from "./value-renderer";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import { useResolvedCoValues } from "./use-resolve-covalue";
|
||||
|
||||
export function TableView({
|
||||
data,
|
||||
node,
|
||||
onNavigate,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
node: LocalNode;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
}) {
|
||||
const [visibleRowsCount, setVisibleRowsCount] = useState(10);
|
||||
const [coIdArray, visibleRows] = useMemo(() => {
|
||||
const coIdArray = Array.isArray(data)
|
||||
? data
|
||||
: Object.values(data).every(
|
||||
(k) => typeof k === "string" && k.startsWith("co_"),
|
||||
)
|
||||
? Object.values(data).map((k) => k as CoID<RawCoValue>)
|
||||
: [];
|
||||
|
||||
const visibleRows = coIdArray.slice(0, visibleRowsCount);
|
||||
|
||||
return [coIdArray, visibleRows];
|
||||
}, [data, visibleRowsCount]);
|
||||
const resolvedRows = useResolvedCoValues(visibleRows, node);
|
||||
|
||||
const hasMore = visibleRowsCount < coIdArray.length;
|
||||
|
||||
if (!coIdArray.length) {
|
||||
return <div>No data to display</div>;
|
||||
}
|
||||
|
||||
if (resolvedRows.length === 0) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
new Set(
|
||||
resolvedRows.flatMap((item) => Object.keys(item.snapshot || {})),
|
||||
),
|
||||
);
|
||||
|
||||
const loadMore = () => {
|
||||
setVisibleRowsCount((prevVisibleRows) => prevVisibleRows + 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 border-b">
|
||||
<tr>
|
||||
{["", ...keys].map((key) => (
|
||||
<th
|
||||
key={key}
|
||||
className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 rounded"
|
||||
>
|
||||
{key}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{resolvedRows
|
||||
.slice(0, visibleRowsCount)
|
||||
.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-1 py-0">
|
||||
<button
|
||||
onClick={() =>
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
])
|
||||
}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 hover:text-blue-500 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<LinkIcon />
|
||||
</button>
|
||||
</td>
|
||||
{keys.map((key) => (
|
||||
<td
|
||||
key={key}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
<ValueRenderer
|
||||
json={
|
||||
(item.snapshot as JsonObject)[
|
||||
key
|
||||
]
|
||||
}
|
||||
onCoIDClick={(coId) => {
|
||||
async function handleClick() {
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!
|
||||
.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
{
|
||||
coId: coId,
|
||||
name: key,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
handleClick();
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="py-4 text-gray-500 flex items-center justify-between gap-2">
|
||||
<span>
|
||||
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
|
||||
{coIdArray.length}
|
||||
</span>
|
||||
{hasMore && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
examples/inspector/src/viewer/type-icon.tsx
Normal file
47
examples/inspector/src/viewer/type-icon.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import {
|
||||
CoJsonType,
|
||||
ExtendedCoJsonType,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
|
||||
export const TypeIcon = ({
|
||||
type,
|
||||
extendedType,
|
||||
}: {
|
||||
type: CoJsonType;
|
||||
extendedType?: ExtendedCoJsonType;
|
||||
}) => {
|
||||
const iconMap: Record<ExtendedCoJsonType | CoJsonType, string> = {
|
||||
record: "{} Record",
|
||||
image: "🖼️ Image",
|
||||
comap: "{} CoMap",
|
||||
costream: "≋ CoStream",
|
||||
colist: "☰ CoList",
|
||||
account: "👤 Account",
|
||||
group: "👥 Group",
|
||||
};
|
||||
|
||||
const iconKey = extendedType || type;
|
||||
const icon = iconMap[iconKey as keyof typeof iconMap];
|
||||
|
||||
return icon ? <span className="font-mono">{icon}</span> : null;
|
||||
};
|
||||
|
||||
export const ResolveIcon = ({
|
||||
coId,
|
||||
node,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
}) => {
|
||||
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
|
||||
|
||||
if (snapshot === "unavailable" && !type) {
|
||||
return <div className="text-gray-600 font-medium">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (!type) return <div className="whitespace-pre w-14 font-mono"> </div>;
|
||||
|
||||
return <TypeIcon type={type} extendedType={extendedType} />;
|
||||
};
|
||||
9
examples/inspector/src/viewer/types.ts
Normal file
9
examples/inspector/src/viewer/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
|
||||
export type PageInfo = {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export const isCoId = (coId: unknown): coId is CoID<RawCoValue> =>
|
||||
typeof coId === "string" && coId.startsWith("co_");
|
||||
107
examples/inspector/src/viewer/use-page-path.ts
Normal file
107
examples/inspector/src/viewer/use-page-path.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { PageInfo } from "./types";
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
|
||||
export function usePagePath(defaultPath?: PageInfo[]) {
|
||||
const [path, setPath] = useState<PageInfo[]>(() => {
|
||||
const hash = window.location.hash.slice(2); // Remove '#/'
|
||||
if (hash) {
|
||||
try {
|
||||
return decodePathFromHash(hash);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse hash:", e);
|
||||
}
|
||||
}
|
||||
return defaultPath || [];
|
||||
});
|
||||
|
||||
const updatePath = useCallback((newPath: PageInfo[]) => {
|
||||
setPath(newPath);
|
||||
const hash = encodePathToHash(newPath);
|
||||
window.location.hash = `#/${hash}`;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const hash = window.location.hash.slice(2);
|
||||
if (hash) {
|
||||
try {
|
||||
const newPath = decodePathFromHash(hash);
|
||||
setPath(newPath);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse hash:", e);
|
||||
}
|
||||
} else if (defaultPath) {
|
||||
setPath(defaultPath);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
return () => window.removeEventListener("hashchange", handleHashChange);
|
||||
}, [defaultPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
defaultPath &&
|
||||
JSON.stringify(path) !== JSON.stringify(defaultPath)
|
||||
) {
|
||||
updatePath(defaultPath);
|
||||
}
|
||||
}, [defaultPath, path, updatePath]);
|
||||
|
||||
const addPages = useCallback(
|
||||
(newPages: PageInfo[]) => {
|
||||
updatePath([...path, ...newPages]);
|
||||
},
|
||||
[path, updatePath],
|
||||
);
|
||||
|
||||
const goToIndex = useCallback(
|
||||
(index: number) => {
|
||||
updatePath(path.slice(0, index + 1));
|
||||
},
|
||||
[path, updatePath],
|
||||
);
|
||||
|
||||
const setPage = useCallback(
|
||||
(coId: CoID<RawCoValue>) => {
|
||||
updatePath([{ coId, name: "Root" }]);
|
||||
},
|
||||
[updatePath],
|
||||
);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (path.length > 1) {
|
||||
updatePath(path.slice(0, path.length - 1));
|
||||
}
|
||||
}, [path, updatePath]);
|
||||
|
||||
return {
|
||||
path,
|
||||
setPage,
|
||||
addPages,
|
||||
goToIndex,
|
||||
goBack,
|
||||
};
|
||||
}
|
||||
|
||||
function encodePathToHash(path: PageInfo[]): string {
|
||||
return path
|
||||
.map((page) => {
|
||||
if (page.name && page.name !== "Root") {
|
||||
return `${page.coId}:${encodeURIComponent(page.name)}`;
|
||||
}
|
||||
return page.coId;
|
||||
})
|
||||
.join("/");
|
||||
}
|
||||
|
||||
function decodePathFromHash(hash: string): PageInfo[] {
|
||||
return hash.split("/").map((segment) => {
|
||||
const [coId, encodedName] = segment.split(":");
|
||||
return {
|
||||
coId,
|
||||
name: encodedName ? decodeURIComponent(encodedName) : undefined,
|
||||
} as PageInfo;
|
||||
});
|
||||
}
|
||||
222
examples/inspector/src/viewer/use-resolve-covalue.ts
Normal file
222
examples/inspector/src/viewer/use-resolve-covalue.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { CoID, LocalNode, RawBinaryCoStream, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type CoJsonType = "comap" | "costream" | "colist";
|
||||
export type ExtendedCoJsonType = "image" | "record" | "account" | "group";
|
||||
|
||||
type JSON = string | number | boolean | null | JSON[] | { [key: string]: JSON };
|
||||
type JSONObject = { [key: string]: JSON };
|
||||
|
||||
type ResolvedImageDefinition = {
|
||||
originalSize: [number, number];
|
||||
placeholderDataURL?: string;
|
||||
[res: `${number}x${number}`]: RawBinaryCoStream["id"];
|
||||
};
|
||||
|
||||
// Type guard for browser image
|
||||
export const isBrowserImage = (
|
||||
coValue: JSONObject,
|
||||
): coValue is ResolvedImageDefinition => {
|
||||
return "originalSize" in coValue && "placeholderDataURL" in coValue;
|
||||
};
|
||||
|
||||
export type ResolvedGroup = {
|
||||
readKey: string;
|
||||
[key: string]: JSON;
|
||||
};
|
||||
|
||||
export const isGroup = (coValue: JSONObject): coValue is ResolvedGroup => {
|
||||
return "readKey" in coValue;
|
||||
};
|
||||
|
||||
export type ResolvedAccount = {
|
||||
profile: {
|
||||
name: string;
|
||||
};
|
||||
[key: string]: JSON;
|
||||
};
|
||||
|
||||
export const isAccount = (coValue: JSONObject): coValue is ResolvedAccount => {
|
||||
return isGroup(coValue) && "profile" in coValue;
|
||||
};
|
||||
|
||||
export async function resolveCoValue(
|
||||
coValueId: CoID<RawCoValue>,
|
||||
node: LocalNode,
|
||||
): Promise<
|
||||
| {
|
||||
value: RawCoValue;
|
||||
snapshot: JSONObject;
|
||||
type: CoJsonType | null;
|
||||
extendedType: ExtendedCoJsonType | undefined;
|
||||
}
|
||||
| {
|
||||
value: undefined;
|
||||
snapshot: "unavailable";
|
||||
type: null;
|
||||
extendedType: undefined;
|
||||
}
|
||||
> {
|
||||
const value = await node.load(coValueId);
|
||||
|
||||
if (value === "unavailable") {
|
||||
return {
|
||||
value: undefined,
|
||||
snapshot: "unavailable",
|
||||
type: null,
|
||||
extendedType: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const snapshot = value.toJSON() as JSONObject;
|
||||
const type = value.type as CoJsonType;
|
||||
|
||||
// Determine extended type
|
||||
let extendedType: ExtendedCoJsonType | undefined;
|
||||
|
||||
if (type === "comap") {
|
||||
if (isBrowserImage(snapshot)) {
|
||||
extendedType = "image";
|
||||
} else if (isAccount(snapshot)) {
|
||||
extendedType = "account";
|
||||
} else if (isGroup(snapshot)) {
|
||||
extendedType = "group";
|
||||
} else {
|
||||
// This check is a bit of a hack
|
||||
// There might be a better way to do this
|
||||
const children = Object.values(snapshot).slice(0, 10);
|
||||
if (
|
||||
children.every(
|
||||
(c) => typeof c === "string" && c.startsWith("co_"),
|
||||
) &&
|
||||
children.length > 3
|
||||
) {
|
||||
extendedType = "record";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
snapshot,
|
||||
type,
|
||||
extendedType,
|
||||
};
|
||||
}
|
||||
|
||||
function subscribeToCoValue(
|
||||
coValueId: CoID<RawCoValue>,
|
||||
node: LocalNode,
|
||||
callback: (result: Awaited<ReturnType<typeof resolveCoValue>>) => void,
|
||||
) {
|
||||
return node.subscribe(coValueId, (value) => {
|
||||
if (value === "unavailable") {
|
||||
callback({
|
||||
value: undefined,
|
||||
snapshot: "unavailable",
|
||||
type: null,
|
||||
extendedType: undefined,
|
||||
});
|
||||
} else {
|
||||
const snapshot = value.toJSON() as JSONObject;
|
||||
const type = value.type as CoJsonType;
|
||||
let extendedType: ExtendedCoJsonType | undefined;
|
||||
|
||||
if (type === "comap") {
|
||||
if (isBrowserImage(snapshot)) {
|
||||
extendedType = "image";
|
||||
} else if (isAccount(snapshot)) {
|
||||
extendedType = "account";
|
||||
} else if (isGroup(snapshot)) {
|
||||
extendedType = "group";
|
||||
} else {
|
||||
const children = Object.values(snapshot).slice(0, 10);
|
||||
if (
|
||||
children.every(
|
||||
(c) => typeof c === "string" && c.startsWith("co_"),
|
||||
) &&
|
||||
children.length > 3
|
||||
) {
|
||||
extendedType = "record";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback({
|
||||
value,
|
||||
snapshot,
|
||||
type,
|
||||
extendedType,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useResolvedCoValue(
|
||||
coValueId: CoID<RawCoValue>,
|
||||
node: LocalNode,
|
||||
) {
|
||||
const [result, setResult] =
|
||||
useState<Awaited<ReturnType<typeof resolveCoValue>>>();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const unsubscribe = subscribeToCoValue(coValueId, node, (newResult) => {
|
||||
if (isMounted) {
|
||||
setResult(newResult);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
unsubscribe();
|
||||
};
|
||||
}, [coValueId, node]);
|
||||
|
||||
return (
|
||||
result || {
|
||||
value: undefined,
|
||||
snapshot: undefined,
|
||||
type: undefined,
|
||||
extendedType: undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useResolvedCoValues(
|
||||
coValueIds: CoID<RawCoValue>[],
|
||||
node: LocalNode,
|
||||
) {
|
||||
const [results, setResults] = useState<
|
||||
Awaited<ReturnType<typeof resolveCoValue>>[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
coValueIds.forEach((coValueId, index) => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
coValueId,
|
||||
node,
|
||||
(newResult) => {
|
||||
if (isMounted) {
|
||||
setResults((prevResults) => {
|
||||
const newResults = [...prevResults];
|
||||
newResults[index] = newResult;
|
||||
return newResults;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
unsubscribes.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [coValueIds, node]);
|
||||
|
||||
return results;
|
||||
}
|
||||
248
examples/inspector/src/viewer/value-renderer.tsx
Normal file
248
examples/inspector/src/viewer/value-renderer.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import {
|
||||
isBrowserImage,
|
||||
resolveCoValue,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// Is there a chance we can pass the actual CoValue here?
|
||||
export function ValueRenderer({
|
||||
json,
|
||||
compact,
|
||||
onCoIDClick,
|
||||
}: {
|
||||
json: JsonValue | undefined;
|
||||
compact?: boolean;
|
||||
onCoIDClick?: (childNode: CoID<RawCoValue>) => void;
|
||||
}) {
|
||||
if (typeof json === "undefined" || json === undefined) {
|
||||
return <span className="text-gray-400">undefined</span>;
|
||||
}
|
||||
|
||||
if (json === null) {
|
||||
return <span className="text-gray-400">null</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "string" && json.startsWith("co_")) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex gap-1 items-center",
|
||||
onCoIDClick &&
|
||||
"text-blue-500 cursor-pointer hover:underline",
|
||||
)}
|
||||
onClick={() => {
|
||||
onCoIDClick?.(json as CoID<RawCoValue>);
|
||||
}}
|
||||
>
|
||||
{json}
|
||||
{onCoIDClick && <LinkIcon />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
return (
|
||||
<span className="text-green-900 font-mono">
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
{json}
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "number") {
|
||||
return <span className="text-purple-500">{json}</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "boolean") {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
json
|
||||
? "text-green-700 bg-green-700/5"
|
||||
: "text-amber-700 bg-amber-500/5",
|
||||
"font-mono",
|
||||
"inline-block px-1 py-0.5 rounded",
|
||||
)}
|
||||
>
|
||||
{json.toString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
return (
|
||||
<span title={JSON.stringify(json)}>
|
||||
Array <span className="text-gray-500">({json.length})</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "object") {
|
||||
return (
|
||||
<span
|
||||
title={JSON.stringify(json, null, 2)}
|
||||
className="inline-block max-w-64 truncate"
|
||||
>
|
||||
{compact ? (
|
||||
<span>
|
||||
Object{" "}
|
||||
<span className="text-gray-500">
|
||||
({Object.keys(json).length})
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
JSON.stringify(json, null, 2)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{String(json)}</span>;
|
||||
}
|
||||
|
||||
export const CoMapPreview = ({
|
||||
coId,
|
||||
node,
|
||||
limit = 6,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<div className="rounded bg-gray-100 animate-pulse whitespace-pre w-24">
|
||||
{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot === "unavailable" && !value) {
|
||||
return <div className="text-gray-500">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (extendedType === "image" && isBrowserImage(snapshot)) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={snapshot.placeholderDataURL}
|
||||
className="size-8 border-2 border-white drop-shadow-md my-2"
|
||||
/>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
|
||||
</span>
|
||||
|
||||
{/* <CoMapPreview coId={value[]} node={node} /> */}
|
||||
{/* <ProgressiveImg image={value}>
|
||||
{({ src }) => <img src={src} className={clsx("w-full")} />}
|
||||
</ProgressiveImg> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (extendedType === "record") {
|
||||
return (
|
||||
<div>
|
||||
Record{" "}
|
||||
<span className="text-gray-500">
|
||||
({Object.keys(snapshot).length})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "colist") {
|
||||
return (
|
||||
<div>
|
||||
List{" "}
|
||||
<span className="text-gray-500">
|
||||
({(snapshot as unknown as []).length})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm flex flex-col gap-2 items-start">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-2">
|
||||
{Object.entries(snapshot)
|
||||
.slice(0, limit)
|
||||
.map(([key, value]) => (
|
||||
<React.Fragment key={key}>
|
||||
<span className="font-medium">{key}: </span>
|
||||
<span>
|
||||
<ValueRenderer json={value} />
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{Object.entries(snapshot).length > limit && (
|
||||
<div className="text-left text-xs text-gray-500 mt-2">
|
||||
{Object.entries(snapshot).length - limit} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function AccountOrGroupPreview({
|
||||
coId,
|
||||
node,
|
||||
showId = false,
|
||||
onClick,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
showId?: boolean;
|
||||
onClick?: (name?: string) => void;
|
||||
}) {
|
||||
const { snapshot, extendedType } = useResolvedCoValue(coId, node);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (extendedType === "account") {
|
||||
resolveCoValue(
|
||||
(snapshot as unknown as { profile: CoID<RawCoValue> }).profile,
|
||||
node,
|
||||
).then(({ snapshot }) => {
|
||||
if (
|
||||
typeof snapshot === "object" &&
|
||||
"name" in snapshot &&
|
||||
typeof snapshot.name === "string"
|
||||
) {
|
||||
setName(snapshot.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [snapshot, node, extendedType]);
|
||||
|
||||
if (!snapshot) return <span>Loading...</span>;
|
||||
if (extendedType !== "account" && extendedType !== "group") {
|
||||
return <span>CoID is not an account or group</span>;
|
||||
}
|
||||
|
||||
const displayName =
|
||||
extendedType === "account" ? name || "Account" : "Group";
|
||||
const displayText = showId ? `${displayName} (${coId})` : displayName;
|
||||
|
||||
const props = onClick
|
||||
? {
|
||||
onClick: () => onClick(displayName),
|
||||
className: "text-blue-500 cursor-pointer hover:underline",
|
||||
}
|
||||
: {
|
||||
className: "text-gray-500",
|
||||
};
|
||||
|
||||
return <span {...props}>{displayText}</span>;
|
||||
}
|
||||
1
examples/inspector/src/vite-env.d.ts
vendored
Normal file
1
examples/inspector/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
75
examples/inspector/tailwind.config.js
Normal file
75
examples/inspector/tailwind.config.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
29
examples/inspector/tsconfig.json
Normal file
29
examples/inspector/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
examples/inspector/tsconfig.node.json
Normal file
10
examples/inspector/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
examples/inspector/vite.config.ts
Normal file
16
examples/inspector/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: false
|
||||
}
|
||||
})
|
||||
19
examples/musicPlayer/.eslintrc.cjs
Normal file
19
examples/musicPlayer/.eslintrc.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier'
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
24
examples/musicPlayer/.gitignore
vendored
Normal file
24
examples/musicPlayer/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
9
examples/musicPlayer/.prettierrc.js
Normal file
9
examples/musicPlayer/.prettierrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 4,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
examples/musicPlayer/CHANGELOG.md
Normal file
1
examples/musicPlayer/CHANGELOG.md
Normal file
@@ -0,0 +1 @@
|
||||
# jazz-example-musicplayer
|
||||
4
examples/musicPlayer/Dockerfile
Normal file
4
examples/musicPlayer/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM caddy:2.7.3-alpine
|
||||
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||
|
||||
COPY ./dist /usr/share/caddy/
|
||||
42
examples/musicPlayer/README.md
Normal file
42
examples/musicPlayer/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Jazz Music Player Example
|
||||
|
||||
Live version: https://example-musicplayer.jazz.tools
|
||||
|
||||
## Installing & running the example locally
|
||||
|
||||
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
|
||||
|
||||
Start by checking out `jazz`
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gardencmp/jazz.git
|
||||
cd jazz/examples/musicPlayer
|
||||
pnpm pack --pack-destination /tmp
|
||||
mkdir -p ~/jazz-examples/musicPlayer # or any other directory
|
||||
tar -xf /tmp/jazz-example-musicPlayer-* --strip-components 1 -C ~/jazz-examples/musicPlayer
|
||||
cd ~/jazz-examples/musicPlayer
|
||||
```
|
||||
|
||||
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Start the dev server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Questions / problems / feedback
|
||||
|
||||
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||
|
||||
## Configuration: sync server
|
||||
|
||||
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||
|
||||
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).
|
||||
16
examples/musicPlayer/components.json
Normal file
16
examples/musicPlayer/components.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/basicComponents",
|
||||
"utils": "@/basicComponents/lib/utils"
|
||||
}
|
||||
}
|
||||
13
examples/musicPlayer/index.html
Normal file
13
examples/musicPlayer/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz - Music Player example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/2_main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
56
examples/musicPlayer/job-template.nomad
Normal file
56
examples/musicPlayer/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
||||
job "example-musicPlayer$BRANCH_SUFFIX" {
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
to = 80
|
||||
}
|
||||
}
|
||||
|
||||
constraint {
|
||||
attribute = "${node.class}"
|
||||
operator = "="
|
||||
value = "mesh"
|
||||
}
|
||||
|
||||
spread {
|
||||
attribute = "${node.datacenter}"
|
||||
weight = 100
|
||||
}
|
||||
|
||||
constraint {
|
||||
distinct_hosts = true
|
||||
}
|
||||
|
||||
task "server" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "$DOCKER_TAG"
|
||||
ports = ["http"]
|
||||
|
||||
auth = {
|
||||
username = "$DOCKER_USER"
|
||||
password = "$DOCKER_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
name = "example-pets$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 50 # MHz
|
||||
memory = 50 # MB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# deploy bump 4
|
||||
44
examples/musicPlayer/package.json
Normal file
44
examples/musicPlayer/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier --write './src/**/*.{ts,tsx}'",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --fix",
|
||||
"*.{js,jsx,mdx,json}": "prettier --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
6
examples/musicPlayer/postcss.config.js
Normal file
6
examples/musicPlayer/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
examples/musicPlayer/public/example.mp3
Normal file
BIN
examples/musicPlayer/public/example.mp3
Normal file
Binary file not shown.
BIN
examples/musicPlayer/public/jazz-logo.png
Normal file
BIN
examples/musicPlayer/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
143
examples/musicPlayer/src/1_schema.ts
Normal file
143
examples/musicPlayer/src/1_schema.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
CoMap,
|
||||
CoList,
|
||||
BinaryCoStream,
|
||||
co,
|
||||
Profile,
|
||||
Account,
|
||||
} from "jazz-tools";
|
||||
import { getAudioFileData } from "./lib/audio/getAudioFileData";
|
||||
|
||||
/** Walkthrough: Defining the data model with CoJSON
|
||||
*
|
||||
* Here, we define our main data model of tasks, lists of tasks and projects
|
||||
* using CoJSON's collaborative map and list types, CoMap & CoList.
|
||||
*
|
||||
* CoMap values and CoLists items can contain:
|
||||
* - arbitrary immutable JSON
|
||||
* - other CoValues
|
||||
**/
|
||||
|
||||
export class MusicTrack extends CoMap {
|
||||
/**
|
||||
* Attributes are defined as class properties
|
||||
* and you can get the types from the `co` module
|
||||
* here we are defining the title and duration for our music track
|
||||
*
|
||||
* Tip: try to follow the co.string defintion to discover the other available primitives!
|
||||
*/
|
||||
title = co.string;
|
||||
duration = co.number;
|
||||
|
||||
/**
|
||||
* With `co.ref` you can define relations between your coValues.
|
||||
*
|
||||
* Attributes are required by default unless you mark them as optional.
|
||||
*/
|
||||
sourceTrack = co.optional.ref(MusicTrack);
|
||||
|
||||
/**
|
||||
* In Jazz you can files using BinaryCoStream.
|
||||
*
|
||||
* As for any other coValue the music files we put inside BinaryCoStream
|
||||
* is available offline and end-to-end encrypted 😉
|
||||
*/
|
||||
file = co.ref(BinaryCoStream);
|
||||
waveform = co.ref(MusicTrackWaveform);
|
||||
}
|
||||
|
||||
export class MusicTrackWaveform extends CoMap {
|
||||
data = co.json<number[]>();
|
||||
}
|
||||
|
||||
/**
|
||||
* CoList is the collaborative version of Array
|
||||
*
|
||||
* They are strongly typed and accept only the type you define here
|
||||
* as "CoList.Of" argument
|
||||
*/
|
||||
export class ListOfTracks extends CoList.Of(co.ref(MusicTrack)) {}
|
||||
|
||||
export class Playlist extends CoMap {
|
||||
title = co.string;
|
||||
tracks = co.ref(ListOfTracks);
|
||||
}
|
||||
|
||||
export class ListOfPlaylists extends CoList.Of(co.ref(Playlist)) {}
|
||||
|
||||
/** The account root is an app-specific per-user private `CoMap`
|
||||
* where you can store top-level objects for that user */
|
||||
export class MusicaAccountRoot extends CoMap {
|
||||
// The root playlist works as container for the tracks that
|
||||
// the user has uploaded
|
||||
rootPlaylist = co.ref(Playlist);
|
||||
// Here we store the list of playlists that the user has created
|
||||
// or that has been invited to
|
||||
playlists = co.ref(ListOfPlaylists);
|
||||
// We store the active track and playlist as coValue here
|
||||
// so when the user reloads the page can see the last played
|
||||
// track and playlist
|
||||
// You can also add the position in time if you want make it possible
|
||||
// to resume the song
|
||||
activeTrack = co.optional.ref(MusicTrack);
|
||||
activePlaylist = co.ref(Playlist);
|
||||
}
|
||||
|
||||
export class MusicaAccount extends Account {
|
||||
profile = co.ref(Profile);
|
||||
root = co.ref(MusicaAccountRoot);
|
||||
|
||||
/** The account migration is run on account creation and on every log-in.
|
||||
* You can use it to set up the account root and any other initial CoValues you need.
|
||||
*
|
||||
* It's a nice way to shape the inital structure of the account data
|
||||
* and add "onboarding" info.
|
||||
*/
|
||||
async migrate(creationProps?: { name: string }) {
|
||||
super.migrate(creationProps);
|
||||
|
||||
if (!this._refs.root) {
|
||||
const ownership = { owner: this };
|
||||
|
||||
const trackFile = await (await fetch("/example.mp3")).blob();
|
||||
const data = await getAudioFileData(trackFile);
|
||||
|
||||
const initialMusicTrack = MusicTrack.create(
|
||||
{
|
||||
file: await BinaryCoStream.createFromBlob(
|
||||
trackFile,
|
||||
ownership,
|
||||
),
|
||||
duration: data.duration,
|
||||
waveform: MusicTrackWaveform.create(
|
||||
{ data: data.waveform },
|
||||
ownership,
|
||||
),
|
||||
title: "Example audio",
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
const tracks = ListOfTracks.create([initialMusicTrack], ownership);
|
||||
const rootPlaylist = Playlist.create(
|
||||
{
|
||||
tracks,
|
||||
title: "",
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
this.root = MusicaAccountRoot.create(
|
||||
{
|
||||
rootPlaylist,
|
||||
playlists: ListOfPlaylists.create([], ownership),
|
||||
activeTrack: initialMusicTrack,
|
||||
activePlaylist: rootPlaylist,
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Walkthrough: Continue with ./2_main.tsx */
|
||||
105
examples/musicPlayer/src/2_main.tsx
Normal file
105
examples/musicPlayer/src/2_main.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createHashRouter, RouterProvider } from "react-router-dom";
|
||||
import { useMediaPlayer } from "./4_useMediaPlayer";
|
||||
import { HomePage } from "./5_HomePage";
|
||||
import { createNewPlaylist, uploadMusicTracks } from "./3_actions";
|
||||
import { PlaylistPage } from "./6_PlaylistPage";
|
||||
import { InvitePage } from "./7_InvitePage";
|
||||
import { Button } from "./basicComponents/Button";
|
||||
import { FileUploadButton } from "./basicComponents/FileUploadButton";
|
||||
import { PlayerControls } from "./components/PlayerControls";
|
||||
import "./index.css";
|
||||
|
||||
import { MusicaAccount } from "@/1_schema";
|
||||
import { createJazzReactContext, DemoAuth } from "jazz-react";
|
||||
|
||||
const syncServer = new URLSearchParams(location.search).get("sync") as `ws://${string}` | `wss://${string}` | undefined;
|
||||
|
||||
/**
|
||||
* Walkthrough: The top-level provider `<Jazz.Provider/>`
|
||||
*
|
||||
* This shows how to use the top-level provider `<Jazz.Provider/>`,
|
||||
* which provides the rest of the app with a controlled account (used through `useAccount` later).
|
||||
* Here we use `DemoAuth` which is great for prototyping you app without wasting time on figuring out
|
||||
* the best way to do auth.
|
||||
*
|
||||
* `<Jazz.Provider/>` also runs our account migration
|
||||
*/
|
||||
const Jazz = createJazzReactContext({
|
||||
auth: DemoAuth({ appName: "Musica Jazz", accountSchema: MusicaAccount }),
|
||||
peer: syncServer ?? "wss://mesh.jazz.tools/?key=you@example.com",
|
||||
});
|
||||
|
||||
export const { useAccount, useCoState, useAcceptInvite } = Jazz;
|
||||
|
||||
function Main() {
|
||||
const mediaPlayer = useMediaPlayer();
|
||||
|
||||
/**
|
||||
* `me` represents the current user account, which will determine
|
||||
* access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||
*/
|
||||
const { me } = useAccount();
|
||||
|
||||
async function handleFileLoad(files: FileList) {
|
||||
if (!me) return;
|
||||
|
||||
/**
|
||||
* Follow this function definition to see how we update
|
||||
* values in Jazz and manage files!
|
||||
*/
|
||||
/** Walkthrough: Continue with ./3_actions.ts */
|
||||
await uploadMusicTracks(me, files);
|
||||
}
|
||||
|
||||
async function handleCreatePlaylist() {
|
||||
if (!me) return;
|
||||
|
||||
const playlist = await createNewPlaylist(me);
|
||||
|
||||
router.navigate(`/playlist/${playlist.id}`);
|
||||
}
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage mediaPlayer={mediaPlayer} />,
|
||||
},
|
||||
{
|
||||
path: "/playlist/:playlistId",
|
||||
element: <PlaylistPage mediaPlayer={mediaPlayer} />,
|
||||
},
|
||||
{
|
||||
path: "/invite/*",
|
||||
element: <InvitePage />,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center bg-gray-300">
|
||||
<img src="jazz-logo.png" className="px-3 h-[20px]" />
|
||||
<div className="text-nowrap">Jazz music player</div>
|
||||
<div className="flex w-full gap-1 justify-end">
|
||||
<FileUploadButton onFileLoad={handleFileLoad}>
|
||||
Add file
|
||||
</FileUploadButton>
|
||||
<Button onClick={handleCreatePlaylist}>
|
||||
Create new playlist
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<RouterProvider router={router} />
|
||||
<PlayerControls mediaPlayer={mediaPlayer} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Jazz.Provider>
|
||||
<Main />
|
||||
</Jazz.Provider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
128
examples/musicPlayer/src/3_actions.ts
Normal file
128
examples/musicPlayer/src/3_actions.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { getAudioFileData } from "@/lib/audio/getAudioFileData";
|
||||
import { BinaryCoStream, Group } from "jazz-tools";
|
||||
import {
|
||||
ListOfTracks,
|
||||
MusicaAccount,
|
||||
MusicTrack,
|
||||
MusicTrackWaveform,
|
||||
Playlist,
|
||||
} from "./1_schema";
|
||||
|
||||
/**
|
||||
* Walkthrough: Actions
|
||||
*
|
||||
* With Jazz is very simple to update the state, you
|
||||
* just mutate the values and we take care of triggering
|
||||
* the updates and sync and persist the values you change.
|
||||
*
|
||||
* We have grouped the complex updates here in an actions file
|
||||
* just to keep them separated from the components.
|
||||
*
|
||||
* Jazz is very unopinionated in this sense and you can adopt the
|
||||
* pattern that best fits your app.
|
||||
*/
|
||||
|
||||
export async function uploadMusicTracks(
|
||||
account: MusicaAccount,
|
||||
files: FileList,
|
||||
) {
|
||||
// The ownership object defines the user that owns the created coValues
|
||||
// by setting the ownership with "account" we configure the coValues to be private
|
||||
const ownership = {
|
||||
owner: account,
|
||||
};
|
||||
|
||||
for (const file of files) {
|
||||
const data = await getAudioFileData(file);
|
||||
|
||||
// We transform the file blob into a BinaryCoStream
|
||||
// making it a collaborative value that is encrypted, easy
|
||||
// to share across devices and users and available offline!
|
||||
const binaryCoStream = await BinaryCoStream.createFromBlob(
|
||||
file,
|
||||
ownership,
|
||||
);
|
||||
|
||||
const musicTrack = MusicTrack.create(
|
||||
{
|
||||
file: binaryCoStream,
|
||||
duration: data.duration,
|
||||
waveform: MusicTrackWaveform.create(
|
||||
{ data: data.waveform },
|
||||
ownership,
|
||||
),
|
||||
title: file.name,
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
// The newly created musicTrack can be associated to the
|
||||
// user track list using a simple push call
|
||||
account.root?.rootPlaylist?.tracks?.push(musicTrack);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createNewPlaylist(account: MusicaAccount) {
|
||||
// Since playlists are meant to be shared we associate them
|
||||
// to a group which will contain the keys required to get
|
||||
// access to the "owned" values
|
||||
const playlistGroup = Group.create({ owner: account });
|
||||
|
||||
const ownership = { owner: playlistGroup };
|
||||
|
||||
const playlist = Playlist.create(
|
||||
{
|
||||
title: "New Playlist",
|
||||
tracks: ListOfTracks.create([], ownership),
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
// Again, we associate the new playlist to the
|
||||
// user by pushing it into the playlists CoList
|
||||
account.root?.playlists?.push(playlist);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
export async function addTrackToPlaylist(
|
||||
playlist: Playlist,
|
||||
track: MusicTrack,
|
||||
account: MusicaAccount | undefined,
|
||||
) {
|
||||
if (!account) return;
|
||||
|
||||
/**
|
||||
* Since musicTracks are created as private values (see uploadMusicTracks)
|
||||
* to make them shareable as part of the playlist we are cloning them
|
||||
* and setting the playlist group as owner of the clone
|
||||
*
|
||||
* In the future it will be possible to "inherit" the parent group so you
|
||||
* won't need to clone values to have this kind of sharing granularity
|
||||
*/
|
||||
const ownership = { owner: playlist._owner };
|
||||
const blob = await BinaryCoStream.loadAsBlob(track._refs.file.id, account);
|
||||
const waveform = await MusicTrackWaveform.load(
|
||||
track._refs.waveform.id,
|
||||
account,
|
||||
{},
|
||||
);
|
||||
|
||||
if (!blob || !waveform) return;
|
||||
|
||||
const trackClone = MusicTrack.create(
|
||||
{
|
||||
file: await BinaryCoStream.createFromBlob(blob, ownership),
|
||||
duration: track.duration,
|
||||
waveform: MusicTrackWaveform.create(
|
||||
{ data: waveform.data },
|
||||
ownership,
|
||||
),
|
||||
title: track.title,
|
||||
sourceTrack: track,
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
playlist.tracks?.push(trackClone);
|
||||
}
|
||||
99
examples/musicPlayer/src/4_useMediaPlayer.ts
Normal file
99
examples/musicPlayer/src/4_useMediaPlayer.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { usePlayMedia } from "@/lib/audio/usePlayMedia";
|
||||
import { usePlayState } from "@/lib/audio/usePlayState";
|
||||
import { useAccount, useCoState } from "./2_main";
|
||||
import { MusicTrack, Playlist } from "@/1_schema";
|
||||
import { useRef, useState } from "react";
|
||||
import { getNextTrack, getPrevTrack } from "./lib/getters";
|
||||
import { BinaryCoStream, ID } from "jazz-tools";
|
||||
|
||||
export function useMediaPlayer() {
|
||||
const { me } = useAccount();
|
||||
|
||||
const playState = usePlayState();
|
||||
const playMedia = usePlayMedia();
|
||||
|
||||
const [loading, setLoading] = useState<ID<MusicTrack> | null>(null);
|
||||
|
||||
const activeTrack = useCoState(MusicTrack, me?.root?._refs.activeTrack?.id);
|
||||
|
||||
// Reference used to avoid out-of-order track loads
|
||||
const lastLoadedTrackId = useRef<ID<MusicTrack> | null>(null);
|
||||
|
||||
async function loadTrack(track: MusicTrack) {
|
||||
if (!me.root) return;
|
||||
|
||||
lastLoadedTrackId.current = track.id;
|
||||
|
||||
setLoading(track.id);
|
||||
|
||||
const file = await BinaryCoStream.loadAsBlob(track._refs.file.id, me);
|
||||
|
||||
if (!file) {
|
||||
setLoading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if another track has been loaded during
|
||||
// the file download
|
||||
if (lastLoadedTrackId.current !== track.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.root.activeTrack = track;
|
||||
|
||||
await playMedia(file);
|
||||
|
||||
setLoading(null);
|
||||
}
|
||||
|
||||
async function playNextTrack() {
|
||||
if (!me?.root) return;
|
||||
|
||||
const track = await getNextTrack(me);
|
||||
|
||||
if (track) {
|
||||
me.root.activeTrack = track;
|
||||
await loadTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
async function playPrevTrack() {
|
||||
if (!me?.root) return;
|
||||
|
||||
const track = await getPrevTrack(me);
|
||||
|
||||
if (track) {
|
||||
await loadTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
async function setActiveTrack(track: MusicTrack, playlist?: Playlist) {
|
||||
if (!me?.root) return;
|
||||
|
||||
if (
|
||||
activeTrack?.id === track.id &&
|
||||
lastLoadedTrackId.current !== null
|
||||
) {
|
||||
playState.toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
me.root.activePlaylist = playlist ?? me.root.rootPlaylist;
|
||||
|
||||
await loadTrack(track);
|
||||
|
||||
if (playState.value === "pause") {
|
||||
playState.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeTrack,
|
||||
setActiveTrack,
|
||||
playNextTrack,
|
||||
playPrevTrack,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
export type MediaPlayer = ReturnType<typeof useMediaPlayer>;
|
||||
60
examples/musicPlayer/src/5_HomePage.tsx
Normal file
60
examples/musicPlayer/src/5_HomePage.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useAccount } from "./2_main";
|
||||
import { MediaPlayer } from "./4_useMediaPlayer";
|
||||
import { Link } from "./basicComponents/Link";
|
||||
import { MusicTrackRow } from "./components/MusicTrackRow";
|
||||
import { usePlayState } from "./lib/audio/usePlayState";
|
||||
|
||||
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
},
|
||||
playlists: [{}],
|
||||
},
|
||||
});
|
||||
|
||||
const tracks = me?.root.rootPlaylist.tracks;
|
||||
|
||||
const playState = usePlayState();
|
||||
const isPlaying = playState.value === "play";
|
||||
|
||||
const playlists = me?.root.playlists;
|
||||
|
||||
return (
|
||||
<>
|
||||
{playlists && playlists.length > 0 && (
|
||||
<div className="p-3">
|
||||
<b>Playlists</b>
|
||||
<div className="flex py-6 gap-6">
|
||||
{playlists.map((playlist) => (
|
||||
<Link
|
||||
key={playlist.id}
|
||||
to={`/playlist/${playlist.id}`}
|
||||
>
|
||||
{playlist.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ul className="flex flex-col">
|
||||
{tracks?.map(
|
||||
(track) =>
|
||||
track && (
|
||||
<MusicTrackRow
|
||||
track={track}
|
||||
key={track.id}
|
||||
isLoading={mediaPlayer.loading === track.id}
|
||||
isPlaying={
|
||||
mediaPlayer.activeTrack?.id === track.id &&
|
||||
isPlaying
|
||||
}
|
||||
onClick={mediaPlayer.setActiveTrack}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
84
examples/musicPlayer/src/6_PlaylistPage.tsx
Normal file
84
examples/musicPlayer/src/6_PlaylistPage.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { createInviteLink } from "jazz-react";
|
||||
import { ID } from "jazz-tools";
|
||||
import { ChangeEvent } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useAccount, useCoState } from "./2_main";
|
||||
import { Playlist } from "./1_schema";
|
||||
import { MediaPlayer } from "./4_useMediaPlayer";
|
||||
import { addTrackToPlaylist } from "./3_actions";
|
||||
import { Button } from "./basicComponents/Button";
|
||||
import { Link } from "./basicComponents/Link";
|
||||
import { MusicTrackRow } from "./components/MusicTrackRow";
|
||||
import { usePlayState } from "./lib/audio/usePlayState";
|
||||
import { AddTracksToPlaylistSection } from "./components/AddTracksToPlaylistSection";
|
||||
|
||||
export function PlaylistPage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const { playlistId } = useParams<{ playlistId: ID<Playlist> }>();
|
||||
|
||||
const playlist = useCoState(Playlist, playlistId, {
|
||||
tracks: [{}],
|
||||
});
|
||||
|
||||
const { me } = useAccount();
|
||||
|
||||
const playState = usePlayState();
|
||||
const isPlaying = playState.value === "play";
|
||||
|
||||
if (!playlist) return null;
|
||||
|
||||
const handlePlaylistTitleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
playlist.title = evt.target.value;
|
||||
};
|
||||
|
||||
const handlePlaylistShareClick = async () => {
|
||||
if (playlist._owner.myRole() !== "admin") return;
|
||||
|
||||
const inviteLink = createInviteLink(playlist, "reader");
|
||||
|
||||
await navigator.clipboard.writeText(inviteLink);
|
||||
|
||||
alert(`Invite link copied into the clipboard`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex bg-gray-200">
|
||||
<Link to="/">Back</Link>
|
||||
|
||||
<input
|
||||
className="w-full bg-transparent p-1 m-1"
|
||||
value={playlist.title}
|
||||
onChange={handlePlaylistTitleChange}
|
||||
/>
|
||||
|
||||
<Button onClick={handlePlaylistShareClick}>Share</Button>
|
||||
</div>
|
||||
<ul className="flex flex-col py-6">
|
||||
{playlist.tracks?.map(
|
||||
(track) =>
|
||||
track && (
|
||||
<MusicTrackRow
|
||||
track={track}
|
||||
key={track.id}
|
||||
isLoading={mediaPlayer.loading === track.id}
|
||||
isPlaying={
|
||||
mediaPlayer.activeTrack?.id === track.id &&
|
||||
isPlaying
|
||||
}
|
||||
onClick={() => {
|
||||
mediaPlayer.setActiveTrack(track, playlist);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<AddTracksToPlaylistSection
|
||||
playlist={playlist}
|
||||
onTrackClick={(track) =>
|
||||
addTrackToPlaylist(playlist, track, me)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
examples/musicPlayer/src/7_InvitePage.tsx
Normal file
38
examples/musicPlayer/src/7_InvitePage.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ID } from "jazz-tools";
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAcceptInvite, useAccount } from "./2_main";
|
||||
import { Playlist } from "./1_schema";
|
||||
|
||||
export function InvitePage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [],
|
||||
},
|
||||
});
|
||||
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema: Playlist,
|
||||
onAccept: useCallback(
|
||||
async (playlistId: ID<Playlist>) => {
|
||||
if (!me) return;
|
||||
|
||||
const playlist = await Playlist.load(playlistId, me, {});
|
||||
|
||||
if (
|
||||
playlist &&
|
||||
!me.root.playlists.some((item) => playlist.id !== item?.id)
|
||||
) {
|
||||
me.root.playlists.push(playlist);
|
||||
}
|
||||
|
||||
navigate("/playlist/" + playlistId);
|
||||
},
|
||||
[navigate, me],
|
||||
),
|
||||
});
|
||||
|
||||
return <p>Accepting invite....</p>;
|
||||
}
|
||||
20
examples/musicPlayer/src/basicComponents/Button.tsx
Normal file
20
examples/musicPlayer/src/basicComponents/Button.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function Button(props: {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
className={cn(
|
||||
"p-2 bg-blue-300 hover:cursor-pointer flex items-center",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function FileUploadButton(props: {
|
||||
onFileLoad: (files: FileList) => Promise<void>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
async function handleFileLoad(evt: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (!evt.target.files) return;
|
||||
|
||||
await props.onFileLoad(evt.target.files);
|
||||
|
||||
evt.target.value = "";
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="bg-blue-300 hover:cursor-pointer flex items-center">
|
||||
<label className="flex items-center cursor-pointer p-2">
|
||||
<input type="file" onChange={handleFileLoad} multiple hidden />
|
||||
{props.children}
|
||||
</label>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
13
examples/musicPlayer/src/basicComponents/Link.tsx
Normal file
13
examples/musicPlayer/src/basicComponents/Link.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
export function Link(props: { to: string; children: ReactNode }) {
|
||||
return (
|
||||
<RouterLink
|
||||
to={props.to}
|
||||
className="p-2 w-fit bg-blue-300 hover:cursor-pointer flex items-center"
|
||||
>
|
||||
{props.children}
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useAccount, useCoState } from "@/2_main";
|
||||
import { Playlist, MusicTrack, ListOfTracks } from "@/1_schema";
|
||||
import { Button } from "@/basicComponents/Button";
|
||||
import { useState } from "react";
|
||||
|
||||
export function AddTracksToPlaylistSection({
|
||||
playlist,
|
||||
onTrackClick,
|
||||
}: {
|
||||
playlist: Playlist;
|
||||
onTrackClick: (track: MusicTrack) => Promise<void>;
|
||||
}) {
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const listOfTracks = useCoState(ListOfTracks, playlist._refs.tracks.id, []);
|
||||
|
||||
const currentTracksIds = new Set(
|
||||
listOfTracks?.map((track) => track?._refs.sourceTrack?.id),
|
||||
);
|
||||
const tracksToAdd = me?.root.rootPlaylist.tracks.filter(
|
||||
(track) => !currentTracksIds.has(track.id),
|
||||
);
|
||||
|
||||
if (!tracksToAdd?.length) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
Add tracks to the playlist
|
||||
<ul className="flex flex-col px-1 py-6 gap-6">
|
||||
{tracksToAdd.map((track) => (
|
||||
<li
|
||||
key={track.id}
|
||||
className={"flex items-center gap-6 bg-slate-200"}
|
||||
>
|
||||
<AddTrackButton
|
||||
track={track}
|
||||
onTrackClick={onTrackClick}
|
||||
/>
|
||||
{track.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddTrackButton({
|
||||
track,
|
||||
onTrackClick,
|
||||
}: {
|
||||
track: MusicTrack;
|
||||
onTrackClick: (track: MusicTrack) => Promise<void>;
|
||||
}) {
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
async function handleClick() {
|
||||
if (isLoading) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await onTrackClick(track);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className="py-2 px-4" onClick={handleClick}>
|
||||
{isLoading ? <div className="animate-spin">߷</div> : "+"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
55
examples/musicPlayer/src/components/MusicTrackRow.tsx
Normal file
55
examples/musicPlayer/src/components/MusicTrackRow.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { MusicTrack } from "@/1_schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChangeEvent } from "react";
|
||||
|
||||
export function MusicTrackRow({
|
||||
track,
|
||||
isLoading,
|
||||
isPlaying,
|
||||
onClick,
|
||||
}: {
|
||||
track: MusicTrack;
|
||||
isLoading: boolean;
|
||||
isPlaying: boolean;
|
||||
onClick: (track: MusicTrack) => void;
|
||||
}) {
|
||||
function handleTrackTitleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||
track.title = evt.target.value;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={
|
||||
"flex gap-1 hover:bg-slate-200 group py-2 px-2 cursor-pointer"
|
||||
}
|
||||
onClick={() => onClick(track)}
|
||||
>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center justify-center bg-transparent w-8 h-8 ",
|
||||
!isPlaying && "group-hover:bg-slate-300 rounded-full",
|
||||
)}
|
||||
onClick={() => onClick(track)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="animate-spin">߷</div>
|
||||
) : isPlaying ? (
|
||||
"⏸️"
|
||||
) : (
|
||||
"▶️"
|
||||
)}
|
||||
</button>
|
||||
<div className="relative" onClick={(evt) => evt.stopPropagation()}>
|
||||
<input
|
||||
className="absolute w-full h-full left-0 bg-transparent px-1"
|
||||
value={track.title}
|
||||
onChange={handleTrackTitleChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<span className="opacity-0 px-1 w-fit pointer-events-none whitespace-pre">
|
||||
{track.title}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
53
examples/musicPlayer/src/components/PlayerControls.tsx
Normal file
53
examples/musicPlayer/src/components/PlayerControls.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { MediaPlayer } from "@/4_useMediaPlayer";
|
||||
import { usePlayState } from "@/lib/audio/usePlayState";
|
||||
import { Waveform } from "./Waveform";
|
||||
import { useAccount } from "@/2_main";
|
||||
import { useMediaEndListener } from "@/lib/audio/useMediaEndListener";
|
||||
import { useKeyboardListener } from "@/lib/useKeyboardListener";
|
||||
|
||||
export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const playState = usePlayState();
|
||||
const isPlaying = playState.value === "play";
|
||||
|
||||
const activePlaylist = useAccount({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
},
|
||||
}).me?.root.activePlaylist;
|
||||
|
||||
useMediaEndListener(mediaPlayer.playNextTrack);
|
||||
useKeyboardListener("Space", () => {
|
||||
if (document.activeElement !== document.body) return;
|
||||
|
||||
playState.toggle();
|
||||
});
|
||||
|
||||
if (!mediaPlayer.activeTrack) return null;
|
||||
|
||||
const activeTrackTitle = mediaPlayer.activeTrack.title;
|
||||
|
||||
const head = activePlaylist?.title
|
||||
? `${activePlaylist.title} / ${activeTrackTitle}`
|
||||
: activeTrackTitle;
|
||||
|
||||
return (
|
||||
<div className=" flex flex-col fixed bottom-0 left-0 border-t-2 w-full p-4 gap-3">
|
||||
<div>Playling: {head}</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex flex-shrink gap-3 text-xl">
|
||||
{" "}
|
||||
<button onClick={mediaPlayer.playPrevTrack}>⏮️</button>
|
||||
{mediaPlayer.loading ? (
|
||||
<div className="animate-spin">߷</div>
|
||||
) : !isPlaying ? (
|
||||
<button onClick={playState.toggle}>▶️</button>
|
||||
) : (
|
||||
<button onClick={playState.toggle}>⏸️</button>
|
||||
)}
|
||||
<button onClick={mediaPlayer.playNextTrack}>⏭️</button>
|
||||
</div>
|
||||
<Waveform track={mediaPlayer.activeTrack} height={30} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
examples/musicPlayer/src/components/Waveform.tsx
Normal file
60
examples/musicPlayer/src/components/Waveform.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useCoState } from "@/2_main";
|
||||
import { MusicTrack, MusicTrackWaveform } from "@/1_schema";
|
||||
import { usePlayerCurrentTime } from "@/lib/audio/usePlayerCurrentTime";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Waveform(props: { track: MusicTrack; height: number }) {
|
||||
const { track, height } = props;
|
||||
const waveformData = useCoState(
|
||||
MusicTrackWaveform,
|
||||
track._refs.waveform.id,
|
||||
{},
|
||||
)?.data;
|
||||
const duration = track.duration;
|
||||
|
||||
const currentTime = usePlayerCurrentTime();
|
||||
|
||||
if (!waveformData) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const barCount = waveformData.length;
|
||||
const activeBar = Math.ceil(barCount * (currentTime.value / duration));
|
||||
|
||||
function seek(i: number) {
|
||||
currentTime.setValue((i / barCount) * duration);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center items-end w-full"
|
||||
style={{
|
||||
height,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{waveformData.map((value, i) => (
|
||||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
onClick={() => seek(i)}
|
||||
className={cn(
|
||||
"w-1 transition-colors rounded-none rounded-t-lg min-h-1",
|
||||
activeBar >= i ? "bg-gray-500" : "bg-gray-300",
|
||||
"hover:bg-black hover:border-1 hover:border-solid hover:border-black",
|
||||
"focus-visible:outline-black focus:outline-none",
|
||||
)}
|
||||
style={{
|
||||
height: height * value,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
examples/musicPlayer/src/index.css
Normal file
76
examples/musicPlayer/src/index.css
Normal file
@@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
56
examples/musicPlayer/src/lib/audio/AudioManager.ts
Normal file
56
examples/musicPlayer/src/lib/audio/AudioManager.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export class AudioManager {
|
||||
mediaElement: HTMLAudioElement;
|
||||
|
||||
audioObjectURL: string | null = null;
|
||||
|
||||
constructor() {
|
||||
const mediaElement = new Audio();
|
||||
|
||||
this.mediaElement = mediaElement;
|
||||
}
|
||||
|
||||
async unloadCurrentAudio() {
|
||||
if (this.audioObjectURL) {
|
||||
URL.revokeObjectURL(this.audioObjectURL);
|
||||
this.audioObjectURL = null;
|
||||
}
|
||||
}
|
||||
|
||||
async loadAudio(file: Blob) {
|
||||
await this.unloadCurrentAudio();
|
||||
|
||||
const { mediaElement } = this;
|
||||
const audioObjectURL = URL.createObjectURL(file);
|
||||
|
||||
this.audioObjectURL = audioObjectURL;
|
||||
|
||||
mediaElement.src = audioObjectURL;
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.mediaElement.ended) {
|
||||
this.mediaElement.fastSeek(0);
|
||||
}
|
||||
|
||||
this.mediaElement.play();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.mediaElement.pause();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.unloadCurrentAudio();
|
||||
this.mediaElement.pause();
|
||||
}
|
||||
}
|
||||
|
||||
const context = createContext<AudioManager>(new AudioManager());
|
||||
|
||||
export function useAudioManager() {
|
||||
return useContext(context);
|
||||
}
|
||||
|
||||
export const AudionManagerProvider = context.Provider;
|
||||
45
examples/musicPlayer/src/lib/audio/getAudioFileData.ts
Normal file
45
examples/musicPlayer/src/lib/audio/getAudioFileData.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export async function getAudioFileData(file: Blob, samples = 200) {
|
||||
const ctx = new AudioContext();
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const decodedAudio = await ctx.decodeAudioData(buffer);
|
||||
|
||||
return {
|
||||
waveform: transformDecodedAudioToWaveformData(decodedAudio, samples),
|
||||
duration: decodedAudio.duration,
|
||||
};
|
||||
}
|
||||
|
||||
const transformDecodedAudioToWaveformData = (
|
||||
audioBuffer: AudioBuffer,
|
||||
samples: number,
|
||||
) => {
|
||||
const rawData = audioBuffer.getChannelData(0); // We only need to work with one channel of data
|
||||
const blockSize = Math.floor(rawData.length / samples); // the number of samples in each subdivision
|
||||
|
||||
const sampledData: number[] = new Array(samples);
|
||||
let max = 0;
|
||||
|
||||
for (let i = 0; i < samples; i++) {
|
||||
const blockStart = blockSize * i; // the location of the first sample in the block
|
||||
let sum = 0;
|
||||
for (let j = 0; j < blockSize; j++) {
|
||||
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
|
||||
}
|
||||
const sampledValue = sum / blockSize; // divide the sum by the block size to get the average
|
||||
|
||||
if (max < sampledValue) {
|
||||
max = sampledValue;
|
||||
}
|
||||
|
||||
sampledData[i] = sampledValue;
|
||||
}
|
||||
|
||||
const multiplier = max ** -1;
|
||||
|
||||
for (let i = 0; i < samples; i++) {
|
||||
sampledData[i] = sampledData[i] * multiplier;
|
||||
}
|
||||
|
||||
return sampledData;
|
||||
};
|
||||
14
examples/musicPlayer/src/lib/audio/useMediaEndListener.ts
Normal file
14
examples/musicPlayer/src/lib/audio/useMediaEndListener.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAudioManager } from "./AudioManager";
|
||||
|
||||
export function useMediaEndListener(callback: () => void) {
|
||||
const audioManager = useAudioManager();
|
||||
|
||||
useEffect(() => {
|
||||
audioManager.mediaElement.addEventListener("ended", callback);
|
||||
|
||||
return () => {
|
||||
audioManager.mediaElement.removeEventListener("ended", callback);
|
||||
};
|
||||
}, [audioManager, callback]);
|
||||
}
|
||||
24
examples/musicPlayer/src/lib/audio/usePlayMedia.ts
Normal file
24
examples/musicPlayer/src/lib/audio/usePlayMedia.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useRef } from "react";
|
||||
import { useAudioManager } from "./AudioManager";
|
||||
|
||||
export function usePlayMedia() {
|
||||
const audioManager = useAudioManager();
|
||||
|
||||
const previousMediaLoad = useRef<Promise<unknown>>();
|
||||
|
||||
async function playMedia(file: Blob) {
|
||||
// Wait for the previous load to finish
|
||||
// to avoid to incur into concurrency issues
|
||||
await previousMediaLoad.current;
|
||||
|
||||
const promise = audioManager.loadAudio(file);
|
||||
|
||||
previousMediaLoad.current = promise;
|
||||
|
||||
await promise;
|
||||
|
||||
audioManager.play();
|
||||
}
|
||||
|
||||
return playMedia;
|
||||
}
|
||||
39
examples/musicPlayer/src/lib/audio/usePlayState.ts
Normal file
39
examples/musicPlayer/src/lib/audio/usePlayState.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useAudioManager } from "./AudioManager";
|
||||
|
||||
export type PlayState = "pause" | "play";
|
||||
|
||||
export function usePlayState() {
|
||||
const audioManager = useAudioManager();
|
||||
const [value, setValue] = useState<PlayState>("pause");
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setValue(audioManager.mediaElement.paused ? "pause" : "play");
|
||||
|
||||
const onPlay = () => {
|
||||
setValue("play");
|
||||
};
|
||||
|
||||
const onPause = () => {
|
||||
setValue("pause");
|
||||
};
|
||||
|
||||
audioManager.mediaElement.addEventListener("play", onPlay);
|
||||
audioManager.mediaElement.addEventListener("pause", onPause);
|
||||
|
||||
return () => {
|
||||
audioManager.mediaElement.removeEventListener("play", onPlay);
|
||||
audioManager.mediaElement.removeEventListener("pause", onPause);
|
||||
};
|
||||
}, [audioManager]);
|
||||
|
||||
function togglePlayState() {
|
||||
if (value === "pause") {
|
||||
audioManager.play();
|
||||
} else {
|
||||
audioManager.pause();
|
||||
}
|
||||
}
|
||||
|
||||
return { value, toggle: togglePlayState };
|
||||
}
|
||||
32
examples/musicPlayer/src/lib/audio/usePlayerCurrentTime.ts
Normal file
32
examples/musicPlayer/src/lib/audio/usePlayerCurrentTime.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useAudioManager } from "./AudioManager";
|
||||
|
||||
export function usePlayerCurrentTime() {
|
||||
const audioManager = useAudioManager();
|
||||
const [value, setValue] = useState<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setValue(audioManager.mediaElement.currentTime);
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
setValue(audioManager.mediaElement.currentTime);
|
||||
};
|
||||
|
||||
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
|
||||
|
||||
return () => {
|
||||
audioManager.mediaElement.removeEventListener("timeupdate", onTimeUpdate);
|
||||
};
|
||||
}, [audioManager]);
|
||||
|
||||
function setCurrentTime(time: number) {
|
||||
if (audioManager.mediaElement.paused) audioManager.play();
|
||||
|
||||
audioManager.mediaElement.currentTime = time;
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue: setCurrentTime,
|
||||
};
|
||||
}
|
||||
30
examples/musicPlayer/src/lib/getters.ts
Normal file
30
examples/musicPlayer/src/lib/getters.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { MusicaAccount } from "../1_schema";
|
||||
|
||||
export async function getNextTrack(account: MusicaAccount) {
|
||||
if (!account.root?.activePlaylist?.tracks) return;
|
||||
|
||||
const tracks = account.root.activePlaylist.tracks;
|
||||
const activeTrack = account.root._refs.activeTrack;
|
||||
|
||||
const currentIndex = tracks.findIndex(
|
||||
(item) => item?.id === activeTrack.id,
|
||||
);
|
||||
|
||||
const nextIndex = (currentIndex + 1) % tracks.length;
|
||||
|
||||
return tracks[nextIndex];
|
||||
}
|
||||
|
||||
export async function getPrevTrack(account: MusicaAccount) {
|
||||
if (!account.root?.activePlaylist?.tracks) return;
|
||||
|
||||
const tracks = account.root.activePlaylist.tracks;
|
||||
const activeTrack = account.root._refs.activeTrack;
|
||||
|
||||
const currentIndex = tracks.findIndex(
|
||||
(item) => item?.id === activeTrack.id,
|
||||
);
|
||||
|
||||
const previousIndex = (currentIndex - 1 + tracks.length) % tracks.length;
|
||||
return tracks[previousIndex];
|
||||
}
|
||||
16
examples/musicPlayer/src/lib/useKeyboardListener.ts
Normal file
16
examples/musicPlayer/src/lib/useKeyboardListener.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useKeyboardListener(code: string, callback: () => void) {
|
||||
useEffect(() => {
|
||||
const handler = (evt: KeyboardEvent) => {
|
||||
if (evt.code === code) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keyup", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keyup", handler);
|
||||
};
|
||||
}, [callback, code]);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user