Compare commits

..

313 Commits

Author SHA1 Message Date
Anselm
92bccf5974 Release 2024-05-25 17:08:37 +01:00
Anselm
2c1d6dcb8f Fix type signature / depth of useCoState 2024-05-25 17:07:41 +01:00
Anselm Eickhoff
75f45ec0b2 Merge pull request #163 from gardencmp/jazz-schema
New unified API
2024-05-25 16:35:54 +01:00
Anselm
c0ebcadda9 Push tags on publish 2024-05-25 16:28:52 +01:00
Anselm
109e9b6a5b Use changeset for publishing, format 2024-05-25 16:28:10 +01:00
Anselm
d0c3d08e42 Add static homepage build output to standalone server 2024-05-25 16:15:05 +01:00
Anselm
7514830edb Increase homepage memory 2024-05-25 16:06:02 +01:00
Anselm
6f27128b87 Fix homepage port 2024-05-25 15:47:10 +01:00
Anselm
376518d4ef Provide correct nomad template 2024-05-25 15:27:58 +01:00
Anselm
272fc85c13 Fix homepage build 2024-05-25 15:21:57 +01:00
Anselm
579e4f93ee Fix homepage build some more 2024-05-25 15:20:29 +01:00
Anselm
a9accdc3a8 Fix homepage build 2024-05-25 15:19:32 +01:00
Anselm
b403db51d4 Remove typedoc output again 2024-05-25 15:03:45 +01:00
Anselm
108cae037f Homepage build 2024-05-25 14:46:00 +01:00
Anselm
c51897ab9e Release 2024-05-25 14:43:57 +01:00
Anselm
67171cb07a Add runtime option for optional refs 2024-05-25 14:42:33 +01:00
Anselm
6ae3bf6ac9 Release 0.7.0 2024-05-25 11:58:43 +01:00
Anselm
b64b15877a Homepage fixes 2024-05-25 11:53:01 +01:00
Anselm
19f52b7361 Alpha release 2024-05-25 10:32:57 +01:00
Anselm
9dcd15dbc8 Fixed bug with newRandomSessionID being called before crypto was ready 2024-05-25 10:32:33 +01:00
Anselm
a423eeea3b Alpha release 2024-05-24 22:52:34 +01:00
Anselm Eickhoff
99be6a3566 Merge pull request #193 from markmelville/ws_close
ignore error on ws close, fixing "Invalid state: Controller is already closed"
2024-05-24 22:51:33 +01:00
Anselm
e97f730c0f Alpha release 2024-05-24 22:37:22 +01:00
Anselm
f5642335ff Fix useAccount 2024-05-24 22:36:53 +01:00
Anselm
bf0f8ec824 Alpha release 2024-05-24 22:28:50 +01:00
Anselm
fb9ef4ea20 Fix noble curves dep 2024-05-24 22:28:13 +01:00
Anselm
86363197cd Alpha release 2024-05-24 22:11:47 +01:00
Anselm
e93187c971 Homepage improvements 2024-05-24 22:11:32 +01:00
Anselm
1a86e13cf1 Implement deep loading, simplify API 2024-05-24 22:10:46 +01:00
Anselm
b863c1d20a Deep loading based on removing nulls 2024-05-23 17:32:04 +01:00
Anselm
4c8d658c25 Only load each image resolution once 2024-05-21 16:48:14 +01:00
Anselm
69d37437ef Fix infinite recursion in subscriptionScope 2024-05-21 16:47:27 +01:00
Anselm
220bdbae62 Fix type of init param for CoMap.create 2024-05-21 16:46:44 +01:00
Anselm
b23b556b79 Fix readme typo 2024-05-21 14:14:47 +01:00
Anselm
ce721cf3d1 Update main readme 2024-05-21 13:47:48 +01:00
Anselm
41363415fe Add lint-staged 2024-05-21 13:44:26 +01:00
Anselm
b91d0769d5 Add husky pre-commit hook 2024-05-21 13:27:45 +01:00
Anselm
ad16690826 Add minimal package READMEs 2024-05-21 13:24:54 +01:00
Anselm
ca7b81c36a Add eslint to the homepage 2024-05-21 13:09:37 +01:00
Anselm
a632ce1477 Apply prettier and make everything testable from root 2024-05-21 12:10:06 +01:00
Anselm
ca7011e9af Apply eslint to all repos 2024-05-21 10:31:48 +01:00
Anselm
34df432ee8 Apply eslint & prettier to cojson-storage-indexeddb 2024-05-21 09:59:29 +01:00
Anselm
dfa7178041 Set up prettier for cojson 2024-05-21 08:27:09 +01:00
Anselm
a3e9a3b686 Extend eslint rules for cojson 2024-05-21 08:22:17 +01:00
Anselm
599db049f2 Remove dotenv 2024-05-21 07:55:48 +01:00
Anselm
7cba6dd690 Factor out crypto impl and provide pure js one 2024-05-20 20:37:43 +01:00
Anselm
2d406c9d58 Fix minor errors 2024-05-18 16:55:14 +01:00
Anselm
d8fe2b10f1 Alpha release 2024-05-18 16:51:28 +01:00
Anselm
c8343626ba Expose experimental OPFS storage 2024-05-18 14:38:59 +01:00
Anselm
5b188ec093 Alpha release 2024-05-16 18:29:44 +01:00
Anselm
7ebbd80049 Ensure accounts are synced after creation 2024-05-16 18:29:13 +01:00
Anselm
74c9e5d36d More LSM/OPFS progress 2024-05-16 18:16:44 +01:00
Anselm
1a3530747f Add changesets 2024-05-16 10:29:13 +01:00
Anselm
79899b9b18 Update lockfile 2024-05-16 10:29:04 +01:00
Anselm
6ef6a2f507 Implement first devtools formatters 2024-05-16 10:28:50 +01:00
Anselm
ab6d15c9f7 Use fresh subscribe context for useProgressiveImg 2024-05-16 10:28:09 +01:00
Anselm
a0dc9139a2 Add ability to declare minimum required data in subscribe & Improve property access tracing 2024-05-16 10:27:27 +01:00
Anselm
220fa319d5 Use lsm storage from cojson & update effect 2024-05-16 10:25:22 +01:00
Anselm
183d505a5e Remove old cojson-storage-lsm module 2024-05-16 10:24:43 +01:00
Anselm
f960e7e736 Optimizations for incoming sync messages 2024-05-16 10:24:20 +01:00
Anselm
d68ac84e03 WIP working-ish version of LSM storage 2024-05-16 10:22:52 +01:00
Mark Melville
30fbe2b6d7 ignore error on ws close 2024-05-14 15:27:34 -06:00
Anselm
e3a00570e1 Refactor 2024-05-13 15:46:23 +01:00
Anselm
499e02685a Merge branch 'lsm-storage' into jazz-schema 2024-05-13 14:32:37 +01:00
Anselm
6b0418f772 Fix image resolution loading 2024-05-13 14:06:15 +01:00
Anselm
1200aae47d Alpha release 2024-05-12 17:27:56 +01:00
Anselm
20fdb09b33 New cache for covalue proxies 2024-05-12 17:27:08 +01:00
Anselm
522b12dc42 CoJSON performance improvement 2024-05-12 17:24:53 +01:00
Anselm
a7cd0dcce5 Pricing update 2024-05-12 17:24:24 +01:00
Anselm
fd86c11336 Alpha release 2024-05-10 21:02:39 +01:00
Anselm
4fc414d744 Extract jazz cli into jazz-run package 2024-05-10 21:00:20 +01:00
Anselm
6d49e9b06c Alpha release 2024-05-10 17:37:40 +01:00
Anselm
8b866e288b Lots more doc improvements 2024-05-10 17:37:00 +01:00
Anselm
bf22588b0e Small homepage fixes 2024-05-10 14:41:24 +01:00
Anselm
60d5ca2811 Alpha release 2024-05-10 14:21:03 +01:00
Anselm
9e5dcdfa69 Update examples 2024-05-10 14:20:50 +01:00
Anselm
3cc39dd5ed Introduce jazz-tools CLI 2024-05-10 14:20:24 +01:00
Anselm
6878060346 Simplify jazz-nodejs 2024-05-10 14:19:57 +01:00
Anselm
9840061137 Clean up exports 2024-05-10 14:19:35 +01:00
Anselm
6f84e00463 homepage/docs improvements 2024-05-10 14:18:48 +01:00
Anselm
1e82d0d34e Allow fall-through of routes 2024-05-10 14:17:46 +01:00
Anselm
f35bc468b3 Separate pnpm workspace for homepage 2024-05-09 11:44:27 +01:00
Anselm
719071c286 Docs & guide progress 2024-05-09 10:46:33 +01:00
Anselm
c4b439e2e6 Lots of doc improvements 2024-05-07 13:16:27 +01:00
Anselm
77c2b56ceb Alpha release 2024-05-03 15:55:58 +01:00
Anselm
0b17b7ad5a Get rid of self generics, new create syntax 2024-05-03 15:55:32 +01:00
Anselm
db3011a1c9 Homepage improvements 2024-05-03 14:27:07 +01:00
Anselm
b47c695b97 Refactor cojson tests 2024-05-03 13:39:04 +01:00
Anselm
55c1c893ba Homepage improvements 2024-05-02 17:37:02 +01:00
Anselm
bde684fe30 Alpha release 2024-05-02 09:24:42 +01:00
Anselm
89b6c9004b CoValue casting & auto-subbing _owner 2024-05-02 09:23:49 +01:00
Anselm Eickhoff
97cdfbddaf Merge pull request #184 from tobiaslins/addmember-chaining 2024-05-02 07:20:44 +01:00
Tobias Lins
584ee2d136 Return this 2024-05-02 07:54:01 +02:00
Anselm
21771c4725 Alpha release 2024-05-01 21:36:51 +01:00
Anselm
7e8f1bed15 Merge branch 'main' into jazz-schema 2024-05-01 21:35:31 +01:00
Anselm
226ae03603 Better variable naming in test 2024-05-01 15:49:14 +01:00
Anselm
96c494f5ee Alpha release 2024-05-01 15:43:04 +01:00
Anselm
23ba00422f Implement group-based profiles & migrations 2024-05-01 15:42:14 +01:00
Anselm
52675c9c68 Alpha release 2024-05-01 10:36:39 +01:00
Anselm
1b113e0114 Fix CoList.splice / RawCoList.append 2024-05-01 10:36:04 +01:00
Anselm
0a930f5eeb Fix Costream[...].all 2024-05-01 10:35:38 +01:00
Anselm
84f5a83648 Homepage improvements 2024-05-01 10:35:11 +01:00
Anselm
5fa277c254 Alpha release 2024-04-29 13:56:11 +01:00
Anselm
d49c7f2dd4 Fix CoMap.Record.toJSON() 2024-04-29 13:55:23 +01:00
Anselm
a78f1688d9 Alpha release 2024-04-29 13:29:45 +01:00
Anselm
cd37a846d8 Make Account -> Profile a lazy ref schema 2024-04-29 13:28:38 +01:00
Anselm
63374ccb6d Alpha release 2024-04-28 17:27:47 +01:00
Anselm
efe2d91fb3 Make sure delete on CoMaps deletes keys 2024-04-28 17:26:47 +01:00
Anselm
234b2a019b Force cojson tests to pass... 2024-04-28 17:12:12 +01:00
Anselm
4bbcd366bc Relax types of CoMap._schema 2024-04-28 16:59:29 +01:00
Anselm
5724f8747a Fix schema of Account & Group 2024-04-28 16:23:17 +01:00
Anselm
38d44103d1 Alpha release 2024-04-27 18:22:11 +01:00
Anselm
96a7ff68e7 Homepage improvements 2024-04-27 18:20:12 +01:00
Anselm
fb78a55f76 Fix getOwnPropertyDescriptor in _refs proxy 2024-04-27 18:19:57 +01:00
Anselm
fdc7fc7bcf Introduce CoMap.Record 2024-04-27 18:13:48 +01:00
Anselm
ed5643aaf1 Alpha release 2024-04-25 09:35:02 +01:00
Anselm
ac431ef9ef Fix CoMap _refs for co.items 2024-04-25 09:34:08 +01:00
Anselm
0940508637 Homepage improvements 2024-04-25 09:33:21 +01:00
Anselm
704af7d04c Alpha release 2024-04-24 14:07:42 +01:00
Anselm
e4e476a834 Add maxWidth option for loading images 2024-04-24 14:07:18 +01:00
Anselm
ece35b3c6f Alpha release 2024-04-24 13:50:50 +01:00
Anselm
b26eab50b3 Include fast-check as dependency to help dev time resolution 2024-04-24 13:50:00 +01:00
Anselm
b42313a285 Fix subscription to account in useAccount 2024-04-24 13:49:15 +01:00
Anselm
129e2c1668 Alpha release 2024-04-23 21:45:30 +01:00
Anselm
87ddb81562 More precise imports from @effect/schema 2024-04-23 21:45:08 +01:00
Anselm
daee49cd9d Alpha release 2024-04-23 21:25:26 +01:00
Anselm
3aaf773b0a Add missing @scure/bip39 dep 2024-04-23 21:24:31 +01:00
Anselm
460478fc65 Alpha release 2024-04-23 21:17:36 +01:00
Anselm
fe8b5f45b9 Use effect 3.0 2024-04-23 21:17:02 +01:00
Anselm
01ac646c8e Alpha release 2024-04-23 21:10:53 +01:00
Anselm
d4b9fbcc60 Make CoMaps even more subclassable 2024-04-23 21:10:35 +01:00
Anselm
1cfa279543 Alpha release 2024-04-23 20:52:08 +01:00
Anselm
e35be73bcc More superclass-compatible CoMaps 2024-04-23 20:51:36 +01:00
Anselm
f8a5c46e18 Alpha release 2024-04-23 20:27:41 +01:00
Anselm
1c7d85ce76 Fix CoStream types 2024-04-23 20:27:18 +01:00
Anselm
19004b4c36 Alpha release 2024-04-23 20:23:50 +01:00
Anselm
930fa689a7 Add .all to CoStreamEntry 2024-04-23 20:22:50 +01:00
Anselm
18a7b2d6b4 Making progress with docs 2024-04-23 15:16:32 +01:00
Anselm
d2e03ff9d3 Alpha release 2024-04-22 19:30:02 +01:00
Anselm
77a9c8395e Fix variance of ID.__type 2024-04-22 19:29:21 +01:00
Anselm
c4151fcb95 Alpha release 2024-04-22 16:16:37 +01:00
Anselm
4c5c21bba2 Work with stricter TS lints 2024-04-22 16:16:02 +01:00
Anselm
f0f6f1b71c Alpha release 2024-04-22 16:02:21 +01:00
Anselm
a9d6d5a1db Reintroduce jazz-nodejs 2024-04-22 15:58:50 +01:00
Anselm
7849ce6de7 Clean up API even more 2024-04-22 15:47:50 +01:00
Anselm
354bdcdbfb Alpha release 2024-04-19 16:40:18 +01:00
Anselm
8ecd3e88c8 Even friendlier for subclassing CoMap 2024-04-19 16:39:51 +01:00
Anselm
85d2b627f1 Alpha release 2024-04-19 16:28:47 +01:00
Anselm
88fd92e4dc More subclass-friendly types in CoMap 2024-04-19 16:27:48 +01:00
Anselm
952982e7ea Alpha release 2024-04-19 15:28:03 +01:00
Anselm
22e7c27af7 Consistent proxy based API 2024-04-19 15:25:05 +01:00
Anselm
59c18c34de Alpha release 2024-04-17 13:26:56 +01:00
Anselm
6acbaede44 Fix keys 2024-04-17 13:26:23 +01:00
Anselm
1a44f875b3 Alpha release 2024-04-17 13:24:09 +01:00
Anselm
9d935fe1d0 Refactors & homepage improvements 2024-04-17 13:23:31 +01:00
Anselm
e5eed5b9b7 Alpha release 2024-04-16 14:01:57 +01:00
Anselm
05a549f04f Makre refs on list more precise 2024-04-16 14:01:32 +01:00
Anselm
a5e68a4fae Alpha release 2024-04-16 13:58:35 +01:00
Anselm
016a9e342a Move unported packages & make refs type more precise 2024-04-16 13:57:41 +01:00
Anselm
627d8950ae Alpha release 2024-04-16 13:21:46 +01:00
Anselm
770ce08c10 Get rid of Co namespace 2024-04-16 13:17:09 +01:00
Anselm
69ac514b3b New alpha 2024-04-15 16:54:39 +01:00
Anselm
b1481748f9 Boom! 2024-04-15 16:50:14 +01:00
Anselm
49944e323f Just before circular references... 2024-04-08 15:42:47 +01:00
Anselm
15310db389 Fix image loading and CoMap proxy issues 2024-04-05 17:22:46 +01:00
Anselm
ea5c5a2604 Rename jazz-js to jazz-tools 2024-04-05 15:43:23 +01:00
Anselm
e461dd1355 Fix test 2024-04-05 15:32:44 +01:00
Anselm
e299c3e9d8 Alpha 0 prerelease 2024-04-05 15:30:49 +01:00
Anselm
406c47271f Finesse types and fix auth impls 2024-04-05 14:54:03 +01:00
Anselm
05c7efea85 More feature parity for demo apps 2024-04-05 11:45:33 +01:00
Anselm
ce7ddf7055 More stuff to make chat app work 2024-04-04 15:19:50 +01:00
Anselm
beb40b5db6 Mostly make chat example work 2024-03-27 18:21:41 +00:00
Anselm
2def752cc4 Flatten CoValue metadata and add edits for CoList 2024-03-26 15:21:28 +00:00
Anselm
bacf3ae86a Change order of Self generic argument again 2024-03-26 10:52:33 +00:00
Anselm
0fef382f2e Docs & homepage improvements 2024-03-25 14:38:39 +00:00
Anselm
95523d8538 Doc organization & type improvements 2024-03-25 12:20:55 +00:00
Anselm
71f7220bfd Start adding doc comments 2024-03-22 15:25:55 +00:00
Anselm
2212c6deac Fix lots of type issues, implement Account & Group 2024-03-22 14:50:41 +00:00
Anselm
fb3efe4cfd Implement tests for BinaryCoStream 2024-03-21 17:33:15 +00:00
Anselm
e66ac6a7d0 Apply strict mode to tests, finish tests for CoStream 2024-03-21 12:06:05 +00:00
Anselm
7ab3908848 Type cleanup, more CoStream implementation 2024-03-19 17:41:36 +00:00
Anselm
921f1fbfe8 Starting test for CoStream 2024-03-18 11:51:01 +00:00
Anselm
2ac455f8b5 Sketch out more in bindings etc 2024-03-18 11:25:18 +00:00
Anselm
1ce881aed2 jazz-js-efs becomes jazz-js 2024-03-15 14:35:51 +00:00
Anselm
b1b5140951 Sketch out coStream interfaces 2024-03-15 14:34:38 +00:00
Anselm
b109c23233 Change "meta" to "co" 2024-03-15 13:58:49 +00:00
Anselm
a7a34a0b6e Tests for CoList subscription and auto-resolution 2024-03-15 10:44:03 +00:00
Anselm
4bf63934e1 Simple CoList resolution 2024-03-14 16:42:41 +00:00
Anselm
16f572282f Kind of implement splice 2024-03-14 16:15:07 +00:00
Anselm
44380c3700 Implement CoList with primitive items 2024-03-14 15:57:31 +00:00
Anselm
dc46cb1386 Move tests to vitest, fix some 2024-03-14 15:38:27 +00:00
Anselm
3ccb1e8ad7 Sketch imageDef 2024-03-13 14:56:17 +00:00
Anselm
d973c5f48b Pull out common constructor methods 2024-03-13 14:30:46 +00:00
Anselm
f4af78c834 Mostly make index signatures work 2024-03-13 14:16:38 +00:00
Anselm
e6d323fd30 Use correct subclasses at runtime, tests passing 2024-03-12 11:39:25 +00:00
Anselm
e6ab56aeb5 Solve lots of type issues 2024-03-12 11:13:43 +00:00
Anselm
779765b649 Fix a bunch of type issues 2024-03-08 14:45:51 +00:00
Anselm
6da730779a Better CoMapInit 2024-03-08 10:16:00 +00:00
Anselm
a3e77edc57 Support optional references 2024-03-07 16:24:22 +00:00
Anselm
ed00308986 Make CoMap subscription work 2024-03-07 12:29:52 +01:00
Anselm
89e9092e0f Make loading work 2024-03-01 14:27:11 +00:00
Anselm
f8b11754c8 Sketch ValueRefs and loading 2024-02-29 17:39:06 +00:00
Anselm
4b38d0793c Test encoding on assignment 2024-02-29 11:29:16 +00:00
Anselm
b2156f8154 Test with more complicated schema in key 2024-02-29 11:25:17 +00:00
Anselm
3a5422e635 Make CoMaps work with effect schema 2024-02-29 11:23:31 +00:00
Anselm
54d3d76868 move pre-effect-schema implementation of jazz-js away 2024-02-29 11:20:34 +00:00
Anselm Eickhoff
f4dc0ec1b7 Merge pull request #177 from tobiaslins/main 2024-02-27 22:51:51 +00:00
Tobias Lins
f500db2dd3 Implement tree view with nested resolving 2024-02-27 21:56:25 +01:00
Anselm
95f64f9934 Actually deploy inspector 2024-02-27 14:45:54 +00:00
Anselm
cccb0e1a21 First draft of inspector 2024-02-27 14:43:08 +00:00
Anselm
b434a4227f Off by 10x 2024-02-27 11:55:40 +00:00
Anselm
6ba4dc1f04 New pricing 2024-02-27 11:49:49 +00:00
Anselm
2fe4c81d1e Start of EffectTS schema based jazz-js 2024-02-27 11:42:41 +00:00
Anselm
5c00264184 test for enumarting props in CoMap 2024-02-24 11:47:35 +01:00
Anselm
c744849c9b Pull out constructor types from schemas 2024-02-23 14:37:25 +01:00
Anselm
f59b278f00 Make simple coValue resolution in extra keys work 2024-02-22 17:45:36 +01:00
Anselm
b26c155d5f Fix effect types and others 2024-02-22 16:35:43 +01:00
Anselm
6da79b8745 Move owner of coValue to first param of constructor 2024-02-22 16:14:51 +01:00
Anselm
0b92591b17 Sketch first test for comap resolution in extra keys 2024-02-22 15:55:58 +01:00
Anselm
974456db54 Support for primitive extra keys in CoMaps 2024-02-22 14:58:27 +01:00
Anselm
a1326a80fe Completely get rid of immutable map for now 2024-02-21 14:45:19 +01:00
Anselm
00d6946b24 Release 2024-02-14 15:41:37 +00:00
Anselm
c4ffde93c0 Simple reconnect logic for jazz-nodejs 2024-02-13 20:19:51 +00:00
Anselm
37bfe967ea Refactor coValue files into parts 2024-02-09 18:16:44 +00:00
Anselm
9abbbfd6fb Release 2024-02-09 11:16:25 +00:00
Anselm
155cd08e39 Mute most annoying log messages 2024-02-09 11:13:18 +00:00
Anselm
e2e6bdf3bd Sketch for new ImageDefinition 2024-02-09 11:09:26 +00:00
Anselm
810c42c743 Implement BinaryCoStream subscription 2024-02-08 17:04:12 +00:00
Anselm
99e4c1301e Lots of fixes to subscription logic and CoValues 2024-02-08 15:54:03 +00:00
Anselm
8c86a831fc Move coValues into their own folder 2024-02-07 10:02:41 +00:00
Anselm
5e976416a4 Implement and test basic CoStream functionality #171 2024-02-06 16:52:11 +00:00
Anselm
0339e14260 Implementation of CoStreams (WIP) 2024-02-04 17:05:10 +00:00
Anselm
4b94fcebf1 Split up tests 2024-01-18 10:56:25 +00:00
Anselm
ddd2a79f37 Subscription & auto-resolution for CoLists 2024-01-18 10:46:00 +00:00
Anselm
01a8f2dab3 Make nested subscriptions work incl edge cases 2024-01-12 22:14:17 +00:00
Anselm
801629d2c1 Fix type errors 2024-01-09 14:43:33 +00:00
Anselm
87d62c941f Nested coValue loading & introduce EffectTS 2024-01-09 14:37:18 +00:00
Anselm
7e6e0fdcc5 Lots of doc improvements 2024-01-05 16:25:27 +00:00
Anselm
a73b07424c Lots of docs improvements 2024-01-04 17:40:57 +00:00
Anselm
0f9b983132 Very first basic tests 2024-01-04 16:48:51 +00:00
Anselm
43e25902d3 Merge branch 'main' into jazz-schema 2024-01-04 14:37:09 +00:00
Anselm
2c27c8517f Add own lockfile to homepage 2024-01-04 13:45:28 +00:00
Anselm Eickhoff
b496058a0e Merge pull request #154 from gardencmp/pnpm-and-turbo
Move to pnpm and turborepo (deploy)
2024-01-04 13:27:05 +00:00
Anselm Eickhoff
4313663bd1 Merge branch 'main' into pnpm-and-turbo 2024-01-04 13:26:52 +00:00
Anselm Eickhoff
dbdbfbd07a Merge pull request #156 from mirshko/patch-1
Fix loading prop always being false
2024-01-04 13:24:14 +00:00
Anselm
184b23d61f Publish 2024-01-03 11:03:57 +00:00
Anselm
5c03b4f668 Parametrize BrowserImage for maxWidth 2024-01-03 11:01:07 +00:00
Anselm
bdbe777d68 Fix ordering bugs with IndexedDB 2024-01-03 10:59:36 +00:00
Anselm
a838a18647 Publish 2024-01-02 18:37:41 +00:00
Anselm
dd8dba63ea Fix migration changes being lost on loaded account 2024-01-02 18:36:18 +00:00
Jeff Reiner
3f5a664ee7 Fix loading prop always being false 2023-12-22 17:37:38 +01:00
Anselm Eickhoff
707292e1ff Merge pull request #152 from tobiaslins/pnpm-and-turbo
Move to pnpm & turborepo
2023-12-21 16:08:49 +00:00
Anselm
9a81b63943 Use pnpm install 2023-12-21 15:51:25 +00:00
Anselm
30216b7b80 Try GITHUB_TOKEN again 2023-12-21 15:49:47 +00:00
Anselm
b2fc91c2ce Use personal access token for ghcr push 2023-12-21 15:42:10 +00:00
Anselm
ef0328833c Try edited event for PRs 2023-12-21 15:30:35 +00:00
Anselm
6a93f17a4a Try to allow ghcr pushes from contributor PRs 2023-12-21 15:24:56 +00:00
Anselm
01bd07ac66 Use pnpm ci 2023-12-21 14:56:58 +00:00
Anselm
88859cfeca Actually add pnpm to CI 2023-12-21 10:23:58 +00:00
Anselm
dfe563e2bc Use pnpm & turbo for CI build 2023-12-21 10:21:53 +00:00
Anselm
7fc0ff981d Update instructions on "ejecting" examples 2023-12-21 10:15:37 +00:00
Anselm
1a9132102d Get rid of lerna 2023-12-21 09:32:08 +00:00
Anselm Eickhoff
d39638282f Merge branch 'main' into pnpm-and-turbo 2023-12-21 09:21:55 +00:00
Anselm Eickhoff
219071654d Merge pull request #153 from tobiaslins/move-to-localStorage 2023-12-21 07:43:36 +00:00
Tobias Lins
7c415db7bd Move to auth to use localStorage instead of sessionStorage 2023-12-21 08:28:03 +01:00
Tobias Lins
4354c340fc Add more dev exmaples 2023-12-21 08:20:43 +01:00
Anselm
a4b484fa36 Publish 2023-12-20 21:49:54 +00:00
Anselm
3757d12dc4 Fix loading of accounts 2023-12-20 21:48:01 +00:00
Anselm
c3a97b29a9 First test skeleton 2023-12-20 21:15:21 +00:00
Tobias Lins
b65e30ec70 Add changeset & publish commands 2023-12-20 22:04:21 +01:00
Tobias Lins
23a1e0266a Everything is building again 2023-12-20 21:49:31 +01:00
Tobias Lins
76acecfe50 further fixes 2023-12-20 21:18:27 +01:00
Tobias Lins
5031c77afb Update index.tsx 2023-12-20 21:05:33 +01:00
Tobias Lins
af90b8c989 Update vite.config.ts 2023-12-20 21:04:20 +01:00
Tobias Lins
d06b4adad0 Missing dependencies 2023-12-20 21:02:43 +01:00
Tobias Lins
b961cde946 Revert changed files 2023-12-20 20:54:06 +01:00
Tobias Lins
8cbbe2f312 Move to pnpm & turborepo 2023-12-20 20:44:01 +01:00
Anselm
c15a49d82d Merge branch 'main' into jazz-schema 2023-12-20 11:36:09 +00:00
Anselm
fc5b670c73 Publish 2023-12-20 11:33:39 +00:00
Anselm
c8adcc4c47 Fix unused var 2023-12-20 11:32:40 +00:00
Anselm
41a755fe41 publish 2023-12-20 11:17:25 +00:00
Anselm
8def1bb29e IndexedDB & timer perf improvements 2023-12-20 11:04:45 +00:00
Anselm
d379b04e33 Publish 2023-12-18 15:15:27 +00:00
Anselm
17a30e054e Add passphrase based auth + example 2023-12-18 15:13:37 +00:00
Anselm
93809911de more WIP 2023-12-16 16:45:46 +00:00
Anselm
edeb2ca9f4 More WIP 2023-12-13 16:47:56 +00:00
Anselm
01662fc3b8 Merge branch 'main' into jazz-schema 2023-12-06 11:51:28 +00:00
Anselm
7d8f4b4c00 Publish jazz-browser-auth0 2023-12-05 19:12:16 +00:00
Anselm
e2a3896bf0 v0.0.0 2023-12-05 19:11:00 +00:00
Anselm
446de8e0ff Fix for not receiving user_metadata 2023-12-05 19:10:15 +00:00
Anselm
5ae6c95878 Use authorization params everywhere 2023-12-05 17:07:18 +00:00
Anselm
7cde349a50 Actually deploy auth0 example 2023-12-05 16:45:41 +00:00
Anselm
61e640f574 Fix auth0 example dependencies 2023-12-05 16:43:18 +00:00
Anselm
ed122d9d8e Publish 2023-12-05 16:41:12 +00:00
Anselm
34817f4536 Auth0 integration and example 2023-12-05 16:38:37 +00:00
Anselm
134d2f0fda work in progress 2023-12-05 12:06:39 +00:00
Anselm
142973827c Merge branch 'main' into jazz-schema 2023-11-28 08:32:52 +00:00
Anselm
0998a0eabf Correct homepage path 2023-11-24 17:09:34 +00:00
Anselm
a96108478b Homepage updates 2023-11-24 17:04:03 +00:00
Anselm
47444888c3 more progress 2023-11-24 16:54:12 +00:00
Anselm
a4769058f4 Merge branch 'main' into jazz-schema 2023-11-08 11:42:49 +00:00
Anselm
a4cf4c40d4 publish 2023-11-08 11:42:36 +00:00
Anselm Eickhoff
934fe4d29b Merge pull request #127 from KyleAMathews/main
fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts
2023-11-07 17:18:02 +00:00
Anselm Eickhoff
408012f2e5 remove redundant peer add 2023-11-07 17:17:41 +00:00
Kyle Mathews
d0078b830e fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts 2023-11-03 14:58:43 -07:00
Anselm
f7f091e18c Simplify CoValue cards 2023-11-02 14:14:58 +00:00
Anselm
a969430247 move homepage to new folder 2023-11-02 12:13:15 +00:00
Anselm Eickhoff
e52948b2b7 Merge pull request #125 from gardencmp/jazz-nodejs
jazz-nodejs MVP
2023-11-02 12:11:56 +00:00
Anselm
53bb1b230b Fix interpolation 2023-11-02 11:53:19 +00:00
Anselm
54e83aeaaa use NOMAD_ADDR secret 2023-11-02 11:43:32 +00:00
Anselm
aa3129cab5 Provide localNode to AccountMigrations 2023-10-27 14:48:36 +01:00
Anselm
90520dddd7 Make addMember and removeMember take loaded Accounts instead of just IDs 2023-10-27 14:30:55 +01:00
Anselm
03eb77070a Allow account migrations to be async 2023-10-27 11:18:41 +01:00
447 changed files with 40108 additions and 60069 deletions

View File

@@ -1,11 +1,24 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"$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-nodejs-ws",
"cojson-storage-indexeddb",
"cojson-storage-sqlite"
]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -11,19 +11,22 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
example: ["chat", "pets", "todo"]
# 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
@@ -35,14 +38,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
@@ -54,40 +53,60 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
# build-homepage:
# runs-on: ubuntu-latest
build-homepage:
runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
steps:
- uses: actions/checkout@v3
with:
submodules: true
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: pnpm/action-setup@v2
with:
version: 8
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: gardencmp
# password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
# - name: Docker Build & Push
# uses: docker/build-push-action@v4
# with:
# context: ./homepage/homepage-jazz
# push: true
# tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
# cache-from: type=gha
# cache-to: type=gha,mode=max
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: gardencmp
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pnpm Install (root)
run: |
pnpm install
working-directory: .
- name: Pnpm Install & Build (homepage)
run: |
pnpm install
pnpm build;
working-directory: ./homepage/homepage
- name: Docker Build & Push
uses: docker/build-push-action@v4
with:
context: ./homepage
push: true
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-examples:
runs-on: ubuntu-latest
needs: build-examples
strategy:
matrix:
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
example: ["chat", "pets", "todo"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
- uses: actions/checkout@v3
@@ -115,38 +134,38 @@ 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 }}
# deploy-homepage:
# runs-on: ubuntu-latest
# needs: build-homepage
deploy-homepage:
runs-on: ubuntu-latest
needs: build-homepage
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
# - uses: gacts/install-nomad@v1
# - name: Tailscale
# uses: tailscale/github-action@v1
# with:
# authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: gacts/install-nomad@v1
- name: Tailscale
uses: tailscale/github-action@v1
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
# - name: Deploy on Nomad
# run: |
# if [ "${{github.ref_name}}" == "main" ]; then
# export BRANCH_SUFFIX="";
# export BRANCH_SUBDOMAIN="";
# else
# export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
# export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
# fi
- name: Deploy on Nomad
run: |
if [ "${{github.ref_name}}" == "main" ]; then
export BRANCH_SUFFIX="";
export BRANCH_SUBDOMAIN="";
else
export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
fi
# export DOCKER_USER=gardencmp;
# export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
# export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
export DOCKER_USER=gardencmp;
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
# 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;
# working-directory: ./homepage/homepage-jazz
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./homepage

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
node_modules
yarn-error.log
lerna-debug.log
docsTmp
docsTmp
.DS_Store
.turbo

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"

View File

@@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

16062
DOCS.md

File diff suppressed because it is too large Load Diff

118
README.md
View File

@@ -1,116 +1,12 @@
# Jazz - instant sync
<sub>Homepage: [jazz.tools](https://jazz.tools) &mdash; Docs: [DOCS.md](./DOCS.md) &mdash; Community & support: [Discord](https://discord.gg/utDMjHYg42) &mdash; Updates: [Twitter](https://twitter.com/jazz_tools) & [Email](https://gcmp.io/news)</sub>
**Jazz is an open-source toolkit for building apps with *secure sync.***
Quickly build and ship apps with:
- **Cross-device sync**
- **Collaborative features** (incl. real-time multiplayer)
- **Instantly reacting UIs**
- Local-first storage & offline support
- File upload and real-time media streaming
# What is *secure sync*?
**Sync** means that, *instead of making API requests*, you:
- **Read and write data as if it was local** &mdash; from anywhere in your app.
- **Always have data synced to wherever it's needed, instantly:** to other devices of the same user, to other users, to your backend, to your local machine for debugging, etc.
**Secure** means that, *instead of relying on your API or DB for access control*, you:
- **Set fine-grained, role-based permissions in `Group`s** that are **synced along with your data**.
- **Permissions *verifiably enforced* everywhere,** using encryption & signatures under the hood.
- **Change roles dynamically** for evolving teams, expiring invite links and more.
# What's special about Jazz?
Compared to other libraries and frameworks for local-first, sync-based or real-time apps, these are some of the things that make Jazz unique:
- **Jazz is a *batteries-included,* vertically integrated toolkit,** offering everything you need to build an app, including auth, permissions, data model, sync, conflict resolution, blob storage, file uploads, real-time media streaming and more.
- **Jazz has a *small API surface* of only a few abstractions to learn,** which combine in powerful ways to implement a broad set of features.
- **Jazz *granularly* loads and caches *only the data that is needed*,** combining *local-first* instant UI reactivity and offline support with the on-demand data efficiency of conventional APIs
- **Jazz supports end-to-end encryption, but doesn't require it,** allowing you to either manage your user's secret keys for them (based on existing auth flows) or letting your users
- **Jazz is based on CoJSON, a soon-to-be *open standard,*** which means that there will be a whole ecosystem of compatible libraries and frameworks in a variety of environments &mdash; and it will be easy to achieve (secure) interop between Jazz/CoJSON-based apps and services.
# Jazz Global Mesh
Jazz is open source and you can run your own sync & storage server, but to really provide you with everything you need, we're also running
**[Jazz Global Mesh](https://jazz.tools/mesh)**, a globally distributed mesh of servers optimized for:
- **Ultra-low-latency sync** (with geo-aware edge caching and optimal routing)
- **Low-cost, reliable storage**
# Jazz - Instant sync
**Jazz Global Mesh is free for small volumes of data** and it's the **default syncing peer,** so you can **start building multi-user Jazz apps with persistent data in minutes,** using only frontend code!
# Getting started
**Jazz is an open-source toolkit for building apps with *distributed state.***
## Example App Walkthrough
- 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)
**For now the best tutorial is the walkthrough of the [Todo List Example App](#todo-list).**
## General Scenarios
### Building a new, entirely sync-based React app
1. Define your data model with [cojson Collaborative Values (CoValues)](./DOCS.md#covalue).
2. Implement permission logic using [cojson Groups](./DOCS.md#group).
3. Build a user interface with [jazz-react](./DOCS.md#jazz-react) and [auto-sub](./DOCS.md#useautosubid).
### Gradually adding sync to an existing React app
Gradually migrate app features to use sync:
1. Define data model for small aspect of your app with [cojson Collaborative Values (CoValues)](./DOCS.md#covalue).
- Schema adapters/importers for Prisma/Drizzle/PostgreSQL introspection coming soon.
2. Map existing permission logic with [cojson Groups](./DOCS.md#group) & integrate existing auth.
- Auth integrations coming soon.
3. Replace some of the React state and API requests in your UI with [jazz-react](./DOCS.md#jazz-react) and [auto-sub](./DOCS.md#useautosubid).
# Example Apps
## Todo List
**A simple collaborative todo list app.**
Live version: https://example-todo.jazz.tools
Source code & walkthrough: [`./examples/todo`](./examples/todo)
Demonstrates:
- Defining a data model with `CoMap`s and `CoList`s
- Creating data and setting permissions with `Group`s
- Fetching, rendering & editing data from nested `CoValue`s with reactive synced queries
## Rate-My-Pet
**A simple social polling app.**
Live version: https://example-pets.jazz.tools
Source code (walkthrough coming soon): [`./examples/pets`](./examples/pets)
Demonstrates:
- Implementing per-account data streams (reactions) with `CoStream`s
- Implementing image upload and progressive image streaming using helpers from `jazz-react-media-images` (on top of CoJSON's `BinaryCoStreams` & `ImageDefinition` convention)
# Documentation & API Reference
For now, docs are hosted in a single well-structured markdown file: [`./DOCS.md`](./DOCS.md).
- [Package Overview](./DOCS.md#overview)
- [`jazz-react` API](./DOCS.md#jazz-react)
- [`cojson` API](./DOCS.md#cojson)
- [`jazz-browser-media-images` API](./DOCS.md#jazz-browser-media-images)
In the future we'll build a dedicated docs page on the Jazz homepage.
----
Copyright 2023 &mdash; Garden Computing, Inc.
Copyright 2024 &mdash; Garden Computing, Inc.

View File

@@ -9,10 +9,5 @@ module.exports = {
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
rules: {},
}

View File

@@ -1,5 +1,461 @@
# jazz-example-chat
## 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

View File

@@ -1,57 +1,35 @@
# Jazz Todo List Example
# Jazz Chat Example
Live version: https://example-todo.jazz.tools
Live version: https://example-chat.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
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 or our multi-package monorepo)
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
npm install
pnpm install
```
Start the dev server:
```bash
npm run dev
pnpm dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
## Walkthrough
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
3. Creating a new todo project: [`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx)
4. Reactively rendering a todo project as a table, adding and editing tasks: [`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx)
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
## 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.
@@ -61,4 +39,4 @@ If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or
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 `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).
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).

View File

@@ -1,14 +1,19 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.46",
"version": "0.0.49",
"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",
@@ -16,9 +21,10 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"hash-slash": "^0.1.3",
"jazz-react": "^0.5.0",
"jazz-react-auth-local": "^0.4.16",
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"jazz-react": "workspace:*",
"cojson": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
@@ -43,6 +49,6 @@
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
"vite": "^5.0.10"
}
}

View File

@@ -1,35 +1,42 @@
import { WithJazz, useJazz, DemoAuth } from 'jazz-react';
import ReactDOM from 'react-dom/client';
import { HashRoute } from 'hash-slash';
import { ChatWindow } from './chatWindow.tsx';
import { Chat } from './dataModel.ts';
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";
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;
ReactDOM.createRoot(document.getElementById('root')!).render(
<WithJazz auth={DemoAuth({ appName: 'Jazz Chat Example' })} apiKey="api_z9d034j3t34ht034ir">
<App />
</WithJazz>,
);
function App() {
return <div className='flex flex-col items-center justify-between w-screen h-screen p-2 dark:bg-black dark:text-white'>
<button onClick={useJazz().logOut} className='rounded mb-5 px-2 py-1 bg-stone-200 dark:bg-stone-800 dark:text-white self-end'>
Log Out
</button>
{HashRoute({
'/': <Home />,
'/chat/:id': (id) => <ChatWindow chatId={id as Chat['id']} />,
}, { reportToParentFrame: true })}
</div>
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>;
}
function Home() {
const { me } = useJazz();
return <button className='rounded py-2 px-4 bg-stone-200 dark:bg-stone-800 dark:text-white my-auto'
onClick={() => {
const group = me.createGroup().addMember('everyone', 'writer');
const chat = group.createList<Chat>();
location.hash = '/chat/' + chat.id;
}}>
Create New Chat
</button>
}
createRoot(document.getElementById("root")!)
.render(<Jazz.Provider><App/></Jazz.Provider>);

View 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 = '';
}} />;
}

View File

@@ -1,9 +0,0 @@
# jazz-example-file-drop
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -1,64 +0,0 @@
# Jazz Todo List Example
Live version: https://example-todo.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
```
(This ensures that you have the example app without git history or our multi-package monorepo)
Install dependencies:
```bash
npm install
```
Start the dev server:
```bash
npm run dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
## Walkthrough
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
3. Creating a new todo project: [`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx)
4. Reactively rendering a todo project as a table, adding and editing tasks: [`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx)
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
## 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 `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -5,6 +5,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',

View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

View File

@@ -1,10 +1,482 @@
# jazz-example-pets
## 0.0.67
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.66
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-browser-media-images@0.7.1
- jazz-react@0.7.1
## 0.0.65
### Patch Changes
- Updated dependencies [8636319]
- 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 [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- 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
- jazz-browser-media-images@0.7.0
- jazz-react@0.7.0
## 0.0.65-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- jazz-browser-media-images@0.7.0-alpha.40
- jazz-react@0.7.0-alpha.42
## 0.0.65-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-browser-media-images@0.7.0-alpha.39
- jazz-react@0.7.0-alpha.41
## 0.0.65-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.65-alpha.39
### Patch Changes
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
- jazz-browser-media-images@0.7.0-alpha.38
## 0.0.65-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
- jazz-browser-media-images@0.7.0-alpha.37
## 0.0.65-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- jazz-browser-media-images@0.7.0-alpha.36
- jazz-tools@0.7.0-alpha.37
## 0.0.65-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
- jazz-browser-media-images@0.7.0-alpha.35
## 0.0.65-alpha.35
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
- jazz-browser-media-images@0.7.0-alpha.34
## 0.0.65-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-browser-media-images@0.7.0-alpha.33
- jazz-react@0.7.0-alpha.34
## 0.0.65-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.65-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
- jazz-browser-media-images@0.7.0-alpha.32
## 0.0.65-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-browser-media-images@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.65-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-browser-media-images@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.65-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- jazz-browser-media-images@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.65-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- jazz-browser-media-images@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.65-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- jazz-browser-media-images@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.65-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-browser-media-images@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.65-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-browser-media-images@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.65-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- jazz-browser-media-images@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.65-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-browser-media-images@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.65-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-browser-media-images@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.65-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
- jazz-browser-media-images@0.7.0-alpha.21
## 0.0.65-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
- jazz-browser-media-images@0.7.0-alpha.20
## 0.0.65-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-browser-media-images@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.65-alpha.18
### Patch Changes
- jazz-browser-media-images@0.7.0-alpha.18
- jazz-react@0.7.0-alpha.18
## 0.0.65-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-browser-media-images@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.65-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-browser-media-images@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.65-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-browser-media-images@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.65-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-browser-media-images@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.65-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-browser-media-images@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.65-alpha.12
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.12
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.65-alpha.11
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.11
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
## 0.0.65-alpha.10
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.10
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
## 0.0.65-alpha.9
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.9
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.65-alpha.8
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.8
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.65-alpha.7
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.7
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
## 0.0.65-alpha.6
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.6
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.65-alpha.5
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.5
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
## 0.0.65-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-browser-media-images@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.65-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-browser-media-images@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.65-alpha.2
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.2
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.65-alpha.1
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.1
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
## 0.0.65-alpha.0
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.0
- jazz-react@0.7.0-alpha.0
- jazz-tools@0.7.0-alpha.0
## 0.0.64
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.6.0
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.5.0
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16
- Updated dependencies
- jazz-browser-media-images@0.5.0
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -4,41 +4,32 @@ Live version: https://example-pets.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
npx degit gardencmp/jazz/examples/pets jazz-example-pets
cd jazz-example-pets
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/pets
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/pets # or any other directory
tar -xf /tmp/jazz-example-pets-* --strip-components 1 -C ~/jazz-examples/pets
cd ~/jazz-examples/pets
```
(This ensures that you have the example app without git history or our multi-package monorepo)
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
npm install
pnpm install
```
Start the dev server:
```bash
npm run dev
pnpm dev
```
## Structure
TODO
## Walkthrough
### Main parts
TODO
### Helpers
TODO
## 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.
@@ -48,4 +39,4 @@ If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or
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 `<WithJazz>` provider component in [./src/0_main.tsx](./src/0_main.tsx).
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).

View File

@@ -1,14 +1,19 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.63",
"version": "0.0.67",
"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": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
@@ -16,9 +21,9 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "^0.5.0",
"jazz-react": "^0.5.0",
"jazz-react-auth-local": "^0.4.16",
"jazz-browser-media-images": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -0,0 +1,60 @@
import {
Account,
CoList,
CoMap,
CoStream,
ImageDefinition,
Profile,
co,
} from "jazz-tools";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of TODO
*
* TODO
**/
export const ReactionTypes = [
"aww",
"love",
"haha",
"wow",
"tiny",
"chonkers",
] as const;
export type ReactionType = (typeof ReactionTypes)[number];
export class PetReactions extends CoStream.Of(co.json<ReactionType>()) {}
export class PetPost extends CoMap {
name = co.string;
image = co.ref(ImageDefinition);
reactions = co.ref(PetReactions);
}
export class ListOfPosts extends CoList.Of(co.ref(PetPost)) {}
export class PetAccountRoot extends CoMap {
posts = co.ref(ListOfPosts);
}
export class PetAccount extends Account {
profile = co.ref(Profile);
root = co.ref(PetAccountRoot);
migrate(creationProps?: { name: string }) {
super.migrate(creationProps);
if (!this._refs.root) {
this.root = PetAccountRoot.create(
{
posts: ListOfPosts.create([], { owner: this }),
},
{ owner: this },
);
console.log("Created root", this.root);
}
}
}
/** Walkthrough: Continue with ./2_App.tsx */

View File

@@ -1,52 +0,0 @@
import {
AccountMigration,
CoList,
CoMap,
CoStream,
Media,
Profile,
} from "cojson";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of TODO
*
* TODO
**/
export type PetPost = CoMap<{
name: string;
image: Media.ImageDefinition["id"];
reactions: PetReactions["id"];
}>;
export const REACTION_TYPES = [
"aww",
"love",
"haha",
"wow",
"tiny",
"chonkers",
] as const;
export type ReactionType = (typeof REACTION_TYPES)[number];
export type PetReactions = CoStream<ReactionType>;
export type ListOfPosts = CoList<PetPost["id"]>;
export type PetAccountRoot = CoMap<{
posts: ListOfPosts["id"];
}>;
export const migration: AccountMigration<Profile, PetAccountRoot> = (account) => {
if (!account.get("root")) {
const root = account.createMap<PetAccountRoot>({
posts: account.createList<ListOfPosts>().id,
});
account.set("root", root.id);
console.log("Created root", root.id);
}
};
/** Walkthrough: Continue with ./2_App.tsx */

View File

@@ -3,8 +3,7 @@ import ReactDOM from "react-dom/client";
import { Link, RouterProvider, createHashRouter } from "react-router-dom";
import "./index.css";
import { WithJazz, useJazz, useAcceptInvite } from "jazz-react";
import { LocalAuth } from "jazz-react-auth-local";
import { createJazzReactContext, PasskeyAuth } from "jazz-react";
import {
Button,
@@ -14,8 +13,7 @@ import {
import { PrettyAuthUI } from "./components/Auth.tsx";
import { NewPetPostForm } from "./3_NewPetPostForm.tsx";
import { RatePetPostUI } from "./4_RatePetPostUI.tsx";
import { PetAccountRoot, migration } from "./1_types.ts";
import { AccountMigration, Profile } from "cojson";
import { PetAccount, PetPost } from "./1_schema.ts";
/** Walkthrough: The top-level provider `<WithJazz/>`
*
@@ -26,22 +24,30 @@ import { AccountMigration, Profile } from "cojson";
const appName = "Jazz Rate My Pet Example";
const auth = LocalAuth({
const auth = PasskeyAuth<PetAccount>({
appName,
Component: PrettyAuthUI,
accountSchema: PetAccount,
});
const Jazz = createJazzReactContext({
auth,
peer: "wss://mesh.jazz.tools/?key=you@example.com",
});
// eslint-disable-next-line react-refresh/only-export-components
export const { useAccount, useCoState, useAcceptInvite } = Jazz;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<WithJazz auth={auth} migration={migration as AccountMigration}>
<Jazz.Provider>
<App />
</WithJazz>
</Jazz.Provider>
</div>
</ThemeProvider>
</React.StrictMode>
</React.StrictMode>,
);
/** Walkthrough: Creating pet posts & routing in `<App/>`
@@ -52,7 +58,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
*/
export default function App() {
const { logOut } = useJazz();
const { logOut } = useAccount();
const router = createHashRouter([
{
@@ -73,7 +79,10 @@ export default function App() {
},
]);
useAcceptInvite((petPostID) => router.navigate("/pet/" + petPostID));
useAcceptInvite({
invitedObjectSchema: PetPost,
onAccept: (petPostID) => router.navigate("/pet/" + petPostID),
});
return (
<>
@@ -90,7 +99,7 @@ export default function App() {
}
export function PostOverview() {
const { me } = useJazz<Profile, PetAccountRoot>();
const { me } = useAccount();
const myPosts = me.root?.posts;
@@ -105,7 +114,7 @@ export function PostOverview() {
<Link key={post.id} to={"/pet/" + post.id}>
{post.name}
</Link>
)
),
)}
</>
) : undefined}

View File

@@ -1,62 +1,63 @@
import { ChangeEvent, useCallback, useState } from "react";
import { useNavigate } from "react-router";
import { createImage } from "jazz-browser-media-images";
import { CoID, CoMap, Media, Profile } from "cojson";
import { useAutoSub, useJazz } from "jazz-react";
import { BrowserImage, createImage } from "jazz-browser-media-images";
import { PetAccountRoot, PetPost, PetReactions } from "./1_types";
import { PetPost, PetReactions } from "./1_schema";
import { Input, Button } from "./basicComponents";
import { useAccount, useCoState } from "./2_main";
import { CoMap, Group, ID, ImageDefinition, co } from "jazz-tools";
import { ProgressiveImg } from "jazz-react";
/** Walkthrough: TODO
*/
type PartialPetPost = CoMap<{
name: string;
image?: Media.ImageDefinition["id"];
reactions: PetReactions["id"];
}>;
class PartialPetPost extends CoMap {
name = co.string;
image = co.ref(ImageDefinition, { optional: true });
reactions = co.ref(PetReactions);
}
export function NewPetPostForm() {
const { me } = useJazz<Profile, PetAccountRoot>();
const { me } = useAccount();
const navigate = useNavigate();
const [newPostId, setNewPostId] = useState<
CoID<PartialPetPost> | undefined
>(undefined);
const [newPostId, setNewPostId] = useState<ID<PartialPetPost> | undefined>(
undefined,
);
const newPetPost = useAutoSub(newPostId);
const newPetPost = useCoState(PartialPetPost, newPostId);
const onChangeName = useCallback(
(name: string) => {
if (newPetPost) {
newPetPost.set({ name });
newPetPost.name = name;
} else {
const petPostGroup = me.createGroup();
const petPost = petPostGroup.createMap<PartialPetPost>({
name,
reactions: petPostGroup.createStream<PetReactions>().id,
});
const petPostGroup = Group.create({ owner: me });
const petPost = PartialPetPost.create(
{
name,
reactions: PetReactions.create([], { owner: me }),
},
{ owner: petPostGroup },
);
setNewPostId(petPost.id);
}
},
[me, newPetPost]
[me, newPetPost],
);
const onImageSelected = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
if (!newPetPost || !event.target.files) return;
const image = await createImage(
event.target.files[0],
newPetPost.meta.group
);
const image = await createImage(event.target.files[0], {
owner: newPetPost._owner,
});
newPetPost.set({ image: image.id });
newPetPost.image = image;
},
[newPetPost]
[newPetPost],
);
const onSubmit = useCallback(() => {
@@ -67,7 +68,7 @@ export function NewPetPostForm() {
throw new Error("No posts list found");
}
myPosts.append(newPetPost.id as PetPost["id"]);
myPosts.push(newPetPost as PetPost);
navigate("/pet/" + newPetPost.id);
}, [me.root?.posts, newPetPost, navigate]);
@@ -84,13 +85,11 @@ export function NewPetPostForm() {
/>
{newPetPost?.image ? (
<img
className="w-80 max-w-full rounded"
src={
newPetPost?.image.as(BrowserImage)
?.highestResSrcOrPlaceholder
}
/>
<ProgressiveImg image={newPetPost.image}>
{({ src }) => (
<img className="w-80 max-w-full rounded" src={src} />
)}
</ProgressiveImg>
) : (
<Input
type="file"

View File

@@ -1,18 +1,20 @@
import { useParams } from "react-router";
import { CoID } from "cojson";
import { PetPost, ReactionType, REACTION_TYPES, PetReactions } from "./1_types";
import { PetPost, PetReactions, ReactionTypes } from "./1_schema";
import { ShareButton } from "./components/ShareButton";
import { Button, Skeleton } from "./basicComponents";
import { BrowserImage } from "jazz-browser-media-images";
import uniqolor from "uniqolor";
import { Resolved, useAutoSub } from "jazz-react";
import { ID } from "jazz-tools";
import { useCoState } from "./2_main";
import { ProgressiveImg } from "jazz-react";
/** Walkthrough: TODO
*/
const reactionEmojiMap: { [reaction in ReactionType]: string } = {
const reactionEmojiMap: {
[reaction in (typeof ReactionTypes)[number]]: string;
} = {
aww: "😍",
love: "❤️",
haha: "😂",
@@ -22,9 +24,9 @@ const reactionEmojiMap: { [reaction in ReactionType]: string } = {
};
export function RatePetPostUI() {
const petPostID = useParams<{ petPostId: CoID<PetPost> }>().petPostId;
const petPostID = useParams<{ petPostId: ID<PetPost> }>().petPostId;
const petPost = useAutoSub(petPostID);
const petPost = useCoState(PetPost, petPostID);
return (
<div className="flex flex-col gap-8">
@@ -33,22 +35,18 @@ export function RatePetPostUI() {
<ShareButton petPost={petPost} />
</div>
{petPost?.image && (
<img
className="w-80 max-w-full rounded"
src={
petPost.image.as(BrowserImage)
?.highestResSrcOrPlaceholder
}
/>
)}
<ProgressiveImg image={petPost?.image}>
{({ src }) => (
<img className="w-80 max-w-full rounded" src={src} />
)}
</ProgressiveImg>
<div className="flex justify-between max-w-xs flex-wrap">
{REACTION_TYPES.map((reactionType) => (
{ReactionTypes.map((reactionType) => (
<Button
key={reactionType}
variant={
petPost?.reactions?.me?.last === reactionType
petPost?.reactions?.byMe?.value === reactionType
? "default"
: "outline"
}
@@ -63,26 +61,22 @@ export function RatePetPostUI() {
))}
</div>
{petPost?.meta.group.myRole() === "admin" && petPost.reactions && (
{petPost?._owner.myRole() === "admin" && petPost.reactions && (
<ReactionOverview petReactions={petPost.reactions} />
)}
</div>
);
}
function ReactionOverview({
petReactions,
}: {
petReactions: Resolved<PetReactions>;
}) {
function ReactionOverview({ petReactions }: { petReactions: PetReactions }) {
return (
<div>
<h2>Reactions</h2>
<div className="flex flex-col gap-1">
{REACTION_TYPES.map((reactionType) => {
const reactionsOfThisType = petReactions.perAccount
.map(([, reaction]) => reaction)
.filter(({ last }) => last === reactionType);
{ReactionTypes.map((reactionType) => {
const reactionsOfThisType = Object.values(
petReactions,
).filter((entry) => entry.value === reactionType);
if (reactionsOfThisType.length === 0) return null;
@@ -106,7 +100,7 @@ function ReactionOverview({
className="mt-1 w-[50px] h-[1em] rounded-full"
key={idx}
/>
)
),
)}
</div>
);

View File

@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -25,7 +25,7 @@ export function ThemeProvider({
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState(
() => localStorage.getItem(storageKey) || defaultTheme
() => localStorage.getItem(storageKey) || defaultTheme,
);
useEffect(() => {
@@ -35,7 +35,7 @@ export function ThemeProvider({
if (theme === "system") {
const systemTheme = window.matchMedia(
"(prefers-color-scheme: dark)"
"(prefers-color-scheme: dark)",
).matches
? "dark"
: "light";
@@ -62,6 +62,7 @@ export function ThemeProvider({
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeProviderContext);

View File

@@ -1,56 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants };

View File

@@ -1,25 +1,25 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,15 +1,15 @@
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
function Skeleton({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,127 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
defaultVariants: {
variant: "default",
},
}
)
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,33 +1,41 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast"
import { useToast } from "@/basicComponents/ui/use-toast"
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast";
import { useToast } from "@/basicComponents/ui/use-toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
return (
<ToastProvider>
{toasts.map(function ({
id,
title,
description,
action,
...props
}) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>
{description}
</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -1,192 +1,193 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast"
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
return {
...state,
toast,
dismiss: (toastId?: string) =>
dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast }
export { useToast, toast };

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { LocalAuthComponent } from "jazz-react-auth-local";
import { PasskeyAuth } from "jazz-react";
import { Input, Button } from "../basicComponents";
export const PrettyAuthUI: LocalAuthComponent = ({
export const PrettyAuthUI: PasskeyAuth.Component = ({
loading,
logIn,
signUp,

View File

@@ -1,18 +1,18 @@
import { useState } from "react";
import { PetPost } from "../1_types";
import { PetPost } from "../1_schema";
import { Resolved, createInviteLink } from "jazz-react";
import { createInviteLink } from "jazz-react";
import QRCode from "qrcode";
import { useToast, Button } from "../basicComponents";
export function ShareButton({ petPost }: { petPost?: Resolved<PetPost> }) {
export function ShareButton({ petPost }: { petPost?: PetPost }) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
petPost?.meta.group.myRole() === "admin" && (
petPost?._owner.myRole() === "admin" && (
<Button
size="sm"
className="py-0"
@@ -34,7 +34,7 @@ export function ShareButton({ petPost }: { petPost?: Resolved<PetPost> }) {
description: (
<img src={qr} className="w-20 h-20" />
),
})
}),
);
}
}}

View File

@@ -5,6 +5,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',

View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

View File

@@ -1,9 +1,431 @@
# jazz-example-todo
## 0.0.66
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.65
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-react@0.7.1
## 0.0.64
### Patch Changes
- Updated dependencies [8636319]
- 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 [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- 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
- jazz-react@0.7.0
## 0.0.64-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- jazz-react@0.7.0-alpha.42
## 0.0.64-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-react@0.7.0-alpha.41
## 0.0.64-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.64-alpha.39
### Patch Changes
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
## 0.0.64-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
## 0.0.64-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- jazz-tools@0.7.0-alpha.37
## 0.0.64-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
## 0.0.64-alpha.35
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
## 0.0.64-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-react@0.7.0-alpha.34
## 0.0.64-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.64-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
## 0.0.64-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.64-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.64-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.64-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.64-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.64-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.64-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.64-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.64-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.64-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.64-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
## 0.0.64-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
## 0.0.64-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.64-alpha.18
### Patch Changes
- jazz-react@0.7.0-alpha.18
## 0.0.64-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.64-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.64-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.64-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.64-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.64-alpha.12
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.64-alpha.11
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
## 0.0.64-alpha.10
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
## 0.0.64-alpha.9
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.64-alpha.8
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.64-alpha.7
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
## 0.0.64-alpha.6
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.64-alpha.5
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
## 0.0.64-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.64-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.64-alpha.2
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.64-alpha.1
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
## 0.0.64-alpha.0
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.0
- cojson@0.7.0-alpha.0
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -4,32 +4,37 @@ Live version: https://example-todo.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/todo
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/todo # or any other directory
tar -xf /tmp/jazz-example-todo-* --strip-components 1 -C ~/jazz-examples/todo
cd ~/jazz-examples/todo
```
(This ensures that you have the example app without git history or our multi-package monorepo)
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
npm install
pnpm install
```
Start the dev server:
```bash
npm run dev
pnpm dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
- [`src/1_schema.ts`](./src/1_schema.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
@@ -38,7 +43,7 @@ npm run dev
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
1. Defining the data model with CoJSON: [`src/1_schema.ts`](./src/1_schema.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
@@ -48,7 +53,7 @@ npm run dev
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
- (not yet explained) Creating Invite Links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
@@ -61,4 +66,4 @@ If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or
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 `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).
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).

View File

@@ -1,14 +1,19 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.63",
"version": "0.0.66",
"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": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
@@ -16,8 +21,8 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.5.0",
"jazz-react-auth-local": "^0.4.16",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
@@ -36,6 +41,7 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",

View File

@@ -0,0 +1,55 @@
import { Account, CoList, CoMap, Profile, co } from "jazz-tools";
/** 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
**/
/** An individual task which collaborators can tick or rename */
export class Task extends CoMap {
done = co.boolean;
text = co.string;
}
export class ListOfTasks extends CoList.Of(co.ref(Task)) {}
/** Our top level object: a project with a title, referencing a list of tasks */
export class TodoProject extends CoMap {
title = co.string;
tasks = co.ref(ListOfTasks);
}
export class ListOfProjects extends CoList.Of(co.ref(TodoProject)) {}
/** The account root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */
export class TodoAccountRoot extends CoMap {
projects = co.ref(ListOfProjects);
}
export class TodoAccount extends Account {
profile = co.ref(Profile);
root = co.ref(TodoAccountRoot);
/** 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.
*/
migrate(creationProps?: { name: string }) {
super.migrate(creationProps);
if (!this._refs.root) {
this.root = TodoAccountRoot.create(
{
projects: ListOfProjects.create([], { owner: this }),
},
{ owner: this },
);
}
}
}
/** Walkthrough: Continue with ./2_main.tsx */

View File

@@ -1,47 +0,0 @@
import { CoMap, CoList, AccountMigration, Profile } from "cojson";
/** 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
* - references to other CoValues by their CoID
**/
/** An individual task which collaborators can tick or rename */
export type Task = CoMap<{ done: boolean; text: string; }>;
export type ListOfTasks = CoList<Task["id"]>;
/** Our top level object: a project with a title, referencing a list of tasks */
export type TodoProject = CoMap<{
title: string;
/** A collaborative, ordered list of tasks */
tasks: ListOfTasks["id"];
}>;
export type ListOfProjects = CoList<TodoProject["id"]>;
/** The account root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */
export type TodoAccountRoot = CoMap<{
projects: ListOfProjects["id"];
}>;
/** 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.
*/
export const migration: AccountMigration<Profile, TodoAccountRoot> = (account) => {
if (!account.get("root")) {
account.set(
"root",
account.createMap<TodoAccountRoot>({
projects: account.createList<ListOfProjects>().id,
}).id
);
}
}
/** Walkthrough: Continue with ./2_main.tsx */

View File

@@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom/client";
import {
RouterProvider,
@@ -7,8 +6,7 @@ import {
} from "react-router-dom";
import "./index.css";
import { WithJazz, useJazz, useAcceptInvite } from "jazz-react";
import { LocalAuth } from "jazz-react-auth-local";
import { createJazzReactContext, PasskeyAuth } from "jazz-react";
import {
Button,
@@ -18,38 +16,44 @@ import {
import { PrettyAuthUI } from "./components/Auth.tsx";
import { NewProjectForm } from "./3_NewProjectForm.tsx";
import { ProjectTodoTable } from "./4_ProjectTodoTable.tsx";
import { TodoAccountRoot, migration } from "./1_types.ts";
import { AccountMigration, Profile } from "cojson";
import { TodoAccount, TodoProject } from "./1_schema.ts";
/**
* Walkthrough: The top-level provider `<WithJazz/>`
* Walkthrough: The top-level provider `<Jazz.Provider/>`
*
* This shows how to use the top-level provider `<WithJazz/>`,
* which provides the rest of the app with a controlled account (used through `useJazz` later).
* Here we use `LocalAuth`, which uses Passkeys (aka WebAuthn) to store a user's account secret
* 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 `PasskeyAuth`, which uses Passkeys (aka WebAuthn) to store a user's account secret
* - no backend needed.
*
* `<WithJazz/>` also runs our account migration
* `<Jazz.Provider/>` also runs our account migration
*/
const appName = "Jazz Todo List Example";
const auth = LocalAuth({
const auth = PasskeyAuth<TodoAccount>({
appName,
Component: PrettyAuthUI,
accountSchema: TodoAccount,
});
const Jazz = createJazzReactContext<TodoAccount>({
auth,
peer: "wss://mesh.jazz.tools/?key=you@example.com",
});
// eslint-disable-next-line react-refresh/only-export-components
export const { useAccount, useCoState, useAcceptInvite } = Jazz;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<WithJazz auth={auth} migration={migration as AccountMigration}>
<App />
</WithJazz>
</div>
</ThemeProvider>
</React.StrictMode>
// <React.StrictMode>
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<Jazz.Provider>
<App />
</Jazz.Provider>
</div>
</ThemeProvider>,
// </React.StrictMode>
);
/**
@@ -59,10 +63,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
* on the CoValue ID (CoID) of our TodoProject, stored in the URL hash
* - which can also contain invite links.
*/
function App() {
// logOut logs out the AuthProvider passed to `<WithJazz/>` above.
const { logOut } = useJazz();
export default function App() {
// logOut logs out the AuthProvider passed to `<Jazz.Provider/>` above.
const { logOut } = useAccount();
const router = createHashRouter([
{
@@ -81,7 +84,11 @@ function App() {
// `useAcceptInvite()` is a hook that accepts an invite link from the URL hash,
// and on success calls our callback where we navigate to the project that we were just invited to.
useAcceptInvite((projectID) => router.navigate("/project/" + projectID));
useAcceptInvite({
invitedObjectSchema: TodoProject,
forValueHint: "project",
onAccept: (projectID) => router.navigate("/project/" + projectID),
});
return (
<>
@@ -97,21 +104,23 @@ function App() {
);
}
export function HomeScreen() {
const { me } = useJazz<Profile, TodoAccountRoot>();
function HomeScreen() {
const { me } = useAccount({
root: { projects: [{}] },
});
const navigate = useNavigate();
return (
<>
{me.root?.projects?.length ? <h1>My Projects</h1> : null}
{me.root?.projects?.map((project) => {
{me?.root.projects.length ? <h1>My Projects</h1> : null}
{me?.root.projects.map((project) => {
return (
<Button
key={project?.id}
key={project.id}
onClick={() => navigate("/project/" + project?.id)}
variant="ghost"
>
{project?.title}
{project.title}
</Button>
);
})}

View File

@@ -1,18 +1,17 @@
import { useCallback } from "react";
import { useJazz } from "jazz-react";
import { ListOfTasks, TodoAccountRoot, TodoProject } from "./1_types";
import { ListOfTasks, TodoProject } from "./1_schema";
import { SubmittableInput } from "./basicComponents";
import { useNavigate } from "react-router";
import { Profile } from "cojson";
import { useAccount } from "./2_main";
import { Group } from "jazz-tools";
export function NewProjectForm() {
// `me` represents the current user account, which will determine
// access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
const { me } = useJazz<Profile, TodoAccountRoot>();
const { me } = useAccount();
const navigate = useNavigate();
const createProject = useCallback(
@@ -22,19 +21,22 @@ export function NewProjectForm() {
// To create a new todo project, we first create a `Group`,
// which is a scope for defining access rights (reader/writer/admin)
// of its members, which will apply to all CoValues owned by that group.
const projectGroup = me.createGroup();
const projectGroup = Group.create({ owner: me });
// Then we create an empty todo project within that group
const project = projectGroup.createMap<TodoProject>({
title,
tasks: projectGroup.createList<ListOfTasks>().id,
});
const project = TodoProject.create(
{
title,
tasks: ListOfTasks.create([], { owner: projectGroup }),
},
{ owner: projectGroup },
);
me.root?.projects?.append(project.id);
me.root?.projects?.push(project);
navigate("/project/" + project.id);
},
[me, navigate]
[me, navigate],
);
return (

View File

@@ -1,8 +1,6 @@
import { useCallback } from "react";
import { CoID } from "cojson";
import { TodoProject, Task } from "./1_types";
import { TodoProject, Task } from "./1_schema";
import {
Checkbox,
@@ -19,7 +17,8 @@ import {
import { InviteButton } from "./components/InviteButton";
import uniqolor from "uniqolor";
import { useParams } from "react-router";
import { Resolved, useAutoSub } from "jazz-react";
import { ID } from "jazz-tools";
import { useCoState } from "./2_main";
/** Walkthrough: Reactively rendering a todo project as a table,
* adding and editing tasks
@@ -30,13 +29,13 @@ import { Resolved, useAutoSub } from "jazz-react";
*/
export function ProjectTodoTable() {
const projectId = useParams<{ projectId: CoID<TodoProject> }>().projectId;
const projectId = useParams<{ projectId: ID<TodoProject> }>().projectId;
// `useAutoSub()` reactively subscribes to updates to a CoValue's
// content - whether we create edits locally, load persisted data, or receive
// sync updates from other devices or participants!
// It also recursively resolves and subsribes to all referenced CoValues.
const project = useAutoSub(projectId);
const project = useCoState(TodoProject, projectId);
// `createTask` is similar to `createProject` we saw earlier, creating a new CoMap
// for a new task (in the same group as the project), and then
@@ -44,17 +43,18 @@ export function ProjectTodoTable() {
const createTask = useCallback(
(text: string) => {
if (!project?.tasks || !text) return;
const task = project.meta.group.createMap<Task>({
done: false,
text,
});
const task = Task.create(
{
done: false,
text,
},
{ owner: project._owner },
);
// project.tasks is immutable, but `append` will create an edit
// that will cause useAutoSub to rerender this component
// - here and on other devices!
project.tasks.append(task.id);
// push will cause useCoState to rerender this component, both here and on other devices
project.tasks.push(task);
},
[project?.tasks, project?.meta.group]
[project?.tasks, project?._owner],
);
return (
@@ -74,7 +74,7 @@ export function ProjectTodoTable() {
)
}
</h1>
<InviteButton value={project} />
<InviteButton value={project} valueHint="project" />
</div>
<Table>
<TableHeader>
@@ -85,7 +85,7 @@ export function ProjectTodoTable() {
</TableHeader>
<TableBody>
{project?.tasks?.map(
(task) => task && <TaskRow key={task.id} task={task} />
(task) => task && <TaskRow key={task.id} task={task} />,
)}
<NewTaskInputRow
createTask={createTask}
@@ -97,7 +97,7 @@ export function ProjectTodoTable() {
);
}
export function TaskRow({ task }: { task: Resolved<Task> | undefined }) {
export function TaskRow({ task }: { task: Task | undefined }) {
return (
<TableRow>
<TableCell>
@@ -108,7 +108,7 @@ export function TaskRow({ task }: { task: Resolved<Task> | undefined }) {
// Tick or untick the task
// Task is also immutable, but this will update all queries
// that include this task as a reference
task?.set({ done: !!checked });
if (task) task.done = !!checked;
}}
/>
</TableCell>
@@ -124,12 +124,12 @@ export function TaskRow({ task }: { task: Resolved<Task> | undefined }) {
{
// Here we see for the first time how we can access edit history
// for a CoValue, and use it to display who created the task.
task?.meta.edits.text?.by?.profile?.name ? (
task?._edits.text?.by?.profile?.name ? (
<span
className="rounded-full py-0.5 px-2 text-xs"
style={uniqueColoring(task.meta.edits.text.by.id)}
style={uniqueColoring(task._edits.text.by.id)}
>
{task.meta.edits.text.by.profile.name}
{task._edits.text.by.profile.name}
</span>
) : (
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />

View File

@@ -18,7 +18,7 @@ export function SubmittableInput({
onSubmit={(e) => {
e.preventDefault();
const textEl = e.currentTarget.elements.namedItem(
"text"
"text",
) as HTMLInputElement;
onSubmit(textEl.value);
textEl.value = "";
@@ -31,7 +31,11 @@ export function SubmittableInput({
autoComplete="off"
disabled={disabled}
/>
<Button asChild type="submit" className="flex-shrink flex-1 cursor-pointer">
<Button
asChild
type="submit"
className="flex-shrink flex-1 cursor-pointer"
>
<Input type="submit" value={label} disabled={disabled} />
</Button>
</form>

View File

@@ -1,10 +1,12 @@
import { Toaster } from ".";
export function TitleAndLogo({name}: {name: string}) {
return <>
<div className="flex items-center gap-2 justify-center mt-5">
export function TitleAndLogo({ name }: { name: string }) {
return (
<>
<div className="flex items-center gap-2 justify-center mt-5">
<img src="jazz-logo.png" className="h-5" /> {name}
</div>
<Toaster />
</>
}
</>
);
}

View File

@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -25,7 +25,7 @@ export function ThemeProvider({
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState(
() => localStorage.getItem(storageKey) || defaultTheme
() => localStorage.getItem(storageKey) || defaultTheme,
);
useEffect(() => {
@@ -35,7 +35,7 @@ export function ThemeProvider({
if (theme === "system") {
const systemTheme = window.matchMedia(
"(prefers-color-scheme: dark)"
"(prefers-color-scheme: dark)",
).matches
? "dark"
: "light";
@@ -62,6 +62,7 @@ export function ThemeProvider({
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeProviderContext);

View File

@@ -1,56 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants };

View File

@@ -1,28 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };

View File

@@ -1,25 +1,25 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,15 +1,15 @@
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
function Skeleton({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,114 +1,120 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
<div className="w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
<tfoot
ref={ref}
className={cn(
"bg-primary font-medium text-primary-foreground",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
<td
ref={ref}
className={cn(
"p-4 align-middle [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -1,127 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
defaultVariants: {
variant: "default",
},
}
)
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,33 +1,41 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast"
import { useToast } from "@/basicComponents/ui/use-toast"
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast";
import { useToast } from "@/basicComponents/ui/use-toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
return (
<ToastProvider>
{toasts.map(function ({
id,
title,
description,
action,
...props
}) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>
{description}
</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -1,192 +1,193 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast"
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
return {
...state,
toast,
dismiss: (toastId?: string) =>
dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast }
export { useToast, toast };

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { LocalAuthComponent } from "jazz-react-auth-local";
import { PasskeyAuth } from "jazz-react";
import { Input, Button } from "../basicComponents";
export const PrettyAuthUI: LocalAuthComponent = ({
export const PrettyAuthUI: PasskeyAuth.Component = ({
loading,
logIn,
signUp,

View File

@@ -3,24 +3,32 @@ import { useState } from "react";
import QRCode from "qrcode";
import { useToast, Button } from "../basicComponents";
import { CoValue } from "cojson";
import { Resolved, createInviteLink } from "jazz-react";
import { createInviteLink } from "jazz-react";
import { CoValue } from "jazz-tools";
export function InviteButton<T extends CoValue>({ value }: { value?: Resolved<T> }) {
export function InviteButton<T extends CoValue>({
value,
valueHint,
}: {
value?: T;
valueHint?: string;
}) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
value?.meta.group?.myRole() === "admin" && (
value?._owner?.myRole() === "admin" && (
<Button
size="sm"
className="py-0"
disabled={!value.meta.group || !value.id}
disabled={!value._owner || !value.id}
variant="outline"
onClick={async () => {
let inviteLink = existingInviteLink;
if (value.meta.group && value.id && !inviteLink) {
inviteLink = createInviteLink(value, "writer");
if (value._owner && value.id && !inviteLink) {
inviteLink = createInviteLink(value, "writer", {
valueHint,
});
setExistingInviteLink(inviteLink);
}
if (inviteLink) {
@@ -33,7 +41,7 @@ export function InviteButton<T extends CoValue>({ value }: { value?: Resolved<T>
description: (
<img src={qr} className="w-20 h-20" />
),
})
}),
);
}
}}

View File

@@ -13,4 +13,4 @@ export default defineConfig({
build: {
minify: false
}
})
})

View File

@@ -1,13 +0,0 @@
# twit-stresstest
## 0.1.0
### Minor Changes
- Adding a lot of performance improvements to cojson, add a stresstest for the twit example and make that run smoother in a lot of ways.
### Patch Changes
- Updated dependencies
- cojson-transport-nodejs-ws@0.5.0
- cojson@0.5.0

View File

@@ -1,64 +0,0 @@
# Jazz Todo List Example
Live version: https://example-todo.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
```
(This ensures that you have the example app without git history or our multi-package monorepo)
Install dependencies:
```bash
npm install
```
Start the dev server:
```bash
npm run dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
## Walkthrough
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
3. Creating a new todo project: [`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx)
4. Reactively rendering a todo project as a table, adding and editing tasks: [`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx)
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
## 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 `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -1,695 +0,0 @@
import { readFile, writeFile } from "fs/promises";
import { Application, JSONOutput, ReflectionKind } from "typedoc";
import { manuallyIgnore, indentEnd, indent } from "./generateDocs";
export async function genDocsMd() {
const packageDocs = Object.entries({
"jazz-react": "index.tsx",
cojson: "index.ts",
"jazz-browser": "index.ts",
"jazz-browser-media-images": "index.ts",
"jazz-autosub": "index.ts",
"jazz-nodejs": "index.ts",
}).map(async ([packageName, entryPoint]) => {
const app = await Application.bootstrapWithPlugins({
entryPoints: [`packages/${packageName}/src/${entryPoint}`],
tsconfig: `packages/${packageName}/tsconfig.json`,
sort: ["required-first"],
groupOrder: ["Functions", "Classes", "TypeAliases", "Namespaces"],
categorizeByGroup: false,
});
const project = await app.convert();
if (!project) {
throw new Error("Failed to convert project" + packageName);
}
// Alternatively generate JSON output
await app.generateJson(project, `docsTmp/${packageName}.json`);
const docs = JSON.parse(
await readFile(`docsTmp/${packageName}.json`, "utf8")
) as JSONOutput.ProjectReflection;
return (
`# ${packageName}\n\n` +
docs
.groups!.map((group) => {
return group.children
?.flatMap((childId) => {
const child = docs.children!.find(
(child) => child.id === childId
)!;
if (manuallyIgnore.has(child.name) ||
child.comment?.blockTags?.some(
(tag) => tag.tag === "@deprecated" ||
tag.tag === "@internal" ||
tag.tag === "@ignore"
) ||
child.signatures?.every((signature) => signature.comment?.blockTags?.some(
(tag) => tag.tag === "@deprecated" ||
tag.tag === "@internal" ||
tag.tag === "@ignore"
)
)) {
return [];
}
return (
`## \`${renderChildName(
child
)}\`\n\n<sup>(${group.title
.toLowerCase()
.replace("bles", "ble")
.replace("ces", "ce")
.replace(/es$/, "")
.replace(
"ns",
"n"
)} in \`${packageName}\`)</sup>\n\n` +
renderChildType(child) +
(child.kind === ReflectionKind.Class ||
child.kind === ReflectionKind.Interface ||
child.kind === ReflectionKind.Namespace
? renderSummary(child.comment) +
renderExamples(child.comment) +
(child.categories || child.groups)
?.map((category) => renderChildCategory(
child,
category
)
)
.join("<br/>\n\n")
: child.kind === ReflectionKind.Function
? renderSummary(
child.signatures?.[0].comment
) +
renderParamComments(
child.signatures?.[0].parameters || []
) +
renderExamples(
child.signatures?.[0].comment
) +
"\n\n"
: "TODO: doc generator not implemented yet " +
child.kind)
);
})
.join("\n\n----\n\n");
})
.join("\n\n----\n\n")
);
function renderSummary(comment?: JSONOutput.Comment): string {
if (comment) {
return (
comment.summary
.map((token) => token.kind === "text" || token.kind === "code"
? token.text
: ""
)
.join("") +
"\n\n" +
"\n\n"
);
} else {
return "TODO: document\n\n";
}
}
function renderExamples(comment?: JSONOutput.Comment): string {
return (comment?.blockTags || [])
.map((blockTag) => blockTag.tag === "@example"
? "##### Example:\n\n" +
blockTag.content
.map((token) => token.kind === "text" || token.kind === "code"
? token.text
: ""
)
.join("") +
"\n\n"
: ""
)
.join("");
}
function renderParamComments(params: JSONOutput.ParameterReflection[]) {
const paramDocs = params.flatMap((param) => {
if (param.type?.type === "reflection") {
return param.type.declaration.children?.flatMap((child) => {
if (child.name === "children" &&
child.type?.type === "reference" &&
child.type?.name === "ReactNode") {
return [];
}
return (
`| \`${param.name}.${child.name}${child.flags.isOptional || child.defaultValue
? "?"
: ""}\` | ` +
(child.comment
? child.comment.summary
.map((token) => token.kind === "text" ||
token.kind === "code"
? token.text
: ""
)
.join("")
: "TODO: document") +
" |"
);
});
} else {
const comment = param.comment;
return [
`| \`${param.name}${param.flags.isOptional || param.defaultValue
? "?"
: ""}\` | ` +
(comment
? comment.summary
.map((token) => token.kind === "text" ||
token.kind === "code"
? token.text
: ""
)
.join("")
: "TODO: document ") +
" |",
];
}
});
if (paramDocs.length) {
return `### Parameters:\n\n| name | description |\n| ----: | ---- |\n${paramDocs.join(
"\n"
)}\n\n`;
}
}
function renderChildName(child: JSONOutput.DeclarationReflection) {
if (child.signatures) {
if (child.signatures[0].type?.type === "reference" &&
child.signatures[0].type.qualifiedName ===
"React.JSX.Element") {
return `<${child.name}/>`;
} else {
return (
child.name +
`(${(child.signatures[0].parameters || [])
.map(renderParamSimple)
.join(", ")})`
);
}
} else {
return child.name;
}
}
function renderChildType(
child: JSONOutput.DeclarationReflection
): string {
const isClass = child.kind === ReflectionKind.Class;
const isTypeAlias = child.kind === ReflectionKind.TypeAlias;
const isInterface = child.kind === ReflectionKind.Interface;
const isNamespace = child.kind === ReflectionKind.Namespace;
const isFunction = !!child.signatures;
const kind = isClass
? "class"
: isTypeAlias
? "type"
: isFunction
? "function"
: isInterface
? "interface"
: isNamespace
? "namespace"
: "";
return (
"```typescript\n" +
`export ${kind} ${child.name}` +
(child.typeParameters || child.signatures?.[0].typeParameter
? "<" +
(
child.typeParameters ||
child.signatures?.[0].typeParameter ||
[]
)
.map(renderTypeParam)
.join(", ") +
">"
: "") +
(child.extendedTypes
? " extends " +
child.extendedTypes.map(renderType).join(", ")
: "") +
(child.implementedTypes
? " implements " +
child.implementedTypes.map(renderType).join(", ")
: "") +
(isClass || isInterface || isNamespace
? " {...}"
: isTypeAlias
? ` = ${renderType(child.type)}`
: child.signatures
? `(${(child.signatures[0].parameters || [])
.map(renderParam)
.join(", ")}): ${renderType(
child.signatures[0].type
)}`
: "") +
"\n```\n"
);
}
function renderChildCategory(
child: JSONOutput.DeclarationReflection,
category: JSONOutput.ReflectionGroup
): string {
return (
`### \`${child.name}\`: ${category.title.replace(
/[^d]+\./,
""
)}\n\n` +
category.children
?.map((memberId) => {
const member = child.children!.find(
(member) => member.id === memberId
)!;
if (member.kind === 2048 || member.kind === 512) {
if (member.signatures?.every(
(sig) => sig.comment?.modifierTags?.includes(
"@internal"
) ||
sig.comment?.modifierTags?.includes(
"@deprecated"
)
)) {
return "";
} else {
return documentConstructorOrMethod(
member,
child
);
}
} else if (member.kind === 1024 ||
member.kind === 262144) {
if (member.comment?.modifierTags?.includes(
"@internal"
) ||
member.comment?.modifierTags?.includes(
"@deprecated"
)) {
return "";
} else {
return documentProperty(member, child);
}
} else if (member.kind === 2097152) {
if (member.comment?.modifierTags?.includes(
"@internal"
) ||
member.comment?.modifierTags?.includes(
"@deprecated"
)) {
return "";
} else {
return documentProperty(
{ ...member, flags: { isStatic: true } },
child
);
}
} else {
return "Unknown member kind " + member.kind;
}
})
.join("\n\n")
);
}
function renderType(t?: JSONOutput.SomeType): string {
if (!t) return "";
if (t.type === "reference") {
return (
t.name +
(t.typeArguments
? "<" + t.typeArguments.map(renderType).join(", ") + ">"
: "")
);
} else if (t.type === "intrinsic") {
return t.name;
} else if (t.type === "literal") {
return JSON.stringify(t.value);
} else if (t.type === "union") {
const seen = new Set<string>();
return t.types
.flatMap((t) => {
const rendered = t.type === "intersection" || t.type === "union"
? `(${renderType(t)})`
: renderType(t);
if (seen.has(rendered)) {
return [];
} else {
seen.add(rendered);
return [rendered];
}
})
.join(" | ");
} else if (t.type === "intersection") {
const seen = new Set<string>();
return t.types
.flatMap((t) => {
const rendered = t.type === "intersection" || t.type === "union"
? `(${renderType(t)})`
: renderType(t);
if (seen.has(rendered)) {
return [];
} else {
seen.add(rendered);
return [rendered];
}
})
.join(" & ");
} else if (t.type === "indexedAccess") {
return (
renderType(t.objectType) +
"[" +
renderType(t.indexType) +
"]"
);
} else if (t.type === "reflection") {
if (t.declaration.indexSignature) {
return (
`{${t.declaration.children
? t.declaration.children
.map(
(child) => ` ${child.name}${child.flags.isOptional
? "?"
: ""}: ${indentEnd(
renderType(child.type)
)},`
)
.join("\n")
: ""}\n [` +
t.declaration.indexSignature?.parameters?.[0].name +
": " +
renderType(
t.declaration.indexSignature?.parameters?.[0].type
) +
"]: " +
indentEnd(
renderType(t.declaration.indexSignature?.type)
) +
" }"
);
} else if (t.declaration.children) {
return `{\n${t.declaration.children
.map((child) => child.signatures
? child.signatures
.map(
(signature) => ` ${child.name}(${signature.parameters
? "\n " +
indent(
signature.parameters
.map((p) => indentEnd(
renderParam(
p
)
)
)
.join(",\n ")
) +
"\n )"
: "()"}: ${indentEnd(
renderType(signature.type)
)}`
)
.join("\n") + ",\n"
: ` ${child.name}${child.flags.isOptional ? "?" : ""}: ${indentEnd(renderType(child.type))},\n`
)
.join("")}}`;
} else if (t.declaration.signatures) {
return t.declaration.signatures
.map(
(signature) => `(${(signature.parameters || [])
.map(renderParam)
.join(", ")}) => ${renderType(
signature.type
)}`
)
.join("\n");
} else {
return "COMPLEX_TYPE_REFLECTION";
}
} else if (t.type === "array") {
return renderType(t.elementType) + "[]";
} else if (t.type === "tuple") {
return `[${t.elements?.map(renderType).join(", ")}]`;
} else if (t.type === "templateLiteral") {
const matchingNamedType = docs.children?.find(
(child) => child.variant === "declaration" &&
child.type?.type === "templateLiteral" &&
child.type.head === t.head &&
child.type.tail.every(
(piece, i) => piece[1] === t.tail[i][1]
)
);
if (matchingNamedType) {
return matchingNamedType.name;
} else {
if (t.head === "sealerSecret_z" &&
t.tail[0][1] === "/signerSecret_z") {
return "AgentSecret";
} else if (t.head === "sealer_z" &&
t.tail[0][1] === "/signer_z") {
if (t.tail[1] && t.tail[1][1] === "_session_z") {
return "SessionID";
} else {
return "AgentID";
}
} else {
return (
"`" +
t.head +
t.tail
.map(
(bit) => "${" + renderType(bit[0]) + "}" + bit[1]
)
.join("") +
"`"
);
}
}
} else if (t.type === "conditional") {
const trueRendered = renderType(t.trueType);
const falseRendered = renderType(t.falseType);
if (trueRendered.includes("\n") ||
falseRendered.includes("\n")) {
return (
renderType(t.checkType) +
" extends " +
renderType(t.extendsType) +
"\n ? " +
indentEnd(renderType(t.trueType)) +
"\n : " +
indentEnd(renderType(t.falseType))
);
} else {
return (
renderType(t.checkType) +
" extends " +
renderType(t.extendsType) +
" ? " +
renderType(t.trueType) +
" : " +
renderType(t.falseType)
);
}
} else if (t.type === "inferred") {
return "infer " + t.name;
} else if (t.type === "typeOperator") {
return t.operator + " " + renderType(t.target);
} else if (t.type === "mapped") {
return `{\n [${t.parameter} in ${renderType(
t.parameterType
)}]: ${indentEnd(renderType(t.templateType))}\n}`;
} else {
return "COMPLEX_TYPE_" + t.type;
}
}
// function renderTemplateLiteral(tempLit: JSONOutput.TemplateLiteralType) {
// return tempLit.head + tempLit.tail.map((piece) => piece[0] + piece[1]).join("");
// }
// function resolveTemplateLiteralPieceType(t: SomeType): string {
// if (t.type === "string") {
// return "${string}"
// }
// if (t.type === "reference") {
// const referencedType = docs.children?.find(
// (child) => child.name === t.name
// );
// }
// }
function renderTypeParam(
t?: JSONOutput.TypeParameterReflection
): string {
if (!t) return "";
return t.name + (t.type ? " extends " + renderType(t.type) : "");
}
function renderParam(param: JSONOutput.ParameterReflection) {
return param.name === "__namedParameters"
? renderType(param.type)
: `${param.name}: ${renderType(param.type)}`;
}
function renderParamSimple(param: JSONOutput.ParameterReflection) {
return param.name === "__namedParameters" &&
param.type?.type === "reflection"
? `{${param.type?.declaration.children
?.map(
(child) => child.name + (child.flags.isOptional ? "?" : "")
)
.join(", ")}}${param.flags.isOptional || param.defaultValue ? "?" : ""}`
: param.name +
(param.flags.isOptional || param.defaultValue ? "?" : "");
}
function documentConstructorOrMethod(
member: JSONOutput.DeclarationReflection,
child: JSONOutput.DeclarationReflection
) {
const isInClass = child.kind === 128;
const isInTypeDef = child.kind === 2097152;
const isInInterface = child.kind === 256;
const isInNamespace = child.kind === 4;
const isInFunction = !!child.signatures;
const inKind = isInClass
? "class"
: isInTypeDef
? "type"
: isInFunction
? "function"
: isInInterface
? "interface"
: isInNamespace
? "namespace"
: "";
const stem = member.name === "constructor"
? "new " + child.name + "</code></b>"
: (member.flags.isStatic ? child.name : "") +
"." +
member.name +
"";
return member.signatures
?.map((signature) => {
return (
`<details>\n<summary><b><code>${stem}(${(
signature?.parameters?.map(renderParamSimple) || []
).join(", ")})</code></b> ${member.inheritedFrom
? "<sub><sup>from <code>" +
member.inheritedFrom.name.split(".")[0] +
"</code></sup></sub> "
: ""} ${signature?.comment
? ""
: "<sub><sup>(undocumented)</sup></sub>"}</summary>\n\n` +
("```typescript\n" +
`${inKind} ${child.name}${child.typeParameters
? `<${child.typeParameters
.map((t) => t.name)
.join(", ")}>`
: ""} {\n\n${indent(
`${member.name}${signature.typeParameter
? `<${signature.typeParameter
.map(renderTypeParam)
.join(", ")}>`
: ""}(${(
signature.parameters?.map(
(param) => `\n ${param.name}${param.flags.isOptional ||
param.defaultValue
? "?"
: ""}: ${indentEnd(
renderType(param.type)
)}${param.defaultValue
? ` = ${param.defaultValue}`
: ""}`
) || []
).join(",") +
(signature.parameters?.length ? "\n" : "")}): ${renderType(signature.type)} {...}`
)}\n\n}\n` +
"```\n" +
renderSummary(signature.comment)) +
renderParamComments(signature.parameters || []) +
renderExamples(signature.comment) +
"</details>\n\n"
);
})
.join("\n\n");
}
function documentProperty(
member: JSONOutput.DeclarationReflection,
child: JSONOutput.DeclarationReflection
) {
const isInClass = child.kind === 128;
const isInTypeDef = child.kind === 2097152;
const isInInterface = child.kind === 256;
const isInNamespace = child.kind === 4;
const isInFunction = !!child.signatures;
const inKind = isInClass
? "class"
: isInTypeDef
? "type"
: isInFunction
? "function"
: isInInterface
? "interface"
: isInNamespace
? "namespace"
: "";
const stem = member.flags.isStatic ? child.name : "";
return (
`<details>\n<summary><b><code>${stem}.${member.name}</code></b> ${member.inheritedFrom
? "<sub><sup>from <code>" +
member.inheritedFrom.name.split(".")[0] +
"</code></sup></sub> "
: ""} ${member.comment ? "" : "<sub><sup>(undocumented)</sup></sub>"}</summary>\n\n` +
"```typescript\n" +
`${inKind} ${child.name}${child.typeParameters
? `<${child.typeParameters
.map((t) => t.name)
.join(", ")}>`
: ""} {\n\n${indent(
`${member.getSignature ? "get " : ""}${member.name}${member.getSignature ? "()" : ""}: ${renderType(member.type || member.getSignature?.type)}${member.getSignature ? " {...}" : ""}`
)}` +
"\n\n}\n```\n" +
renderSummary(member.comment) +
renderExamples(member.comment) +
"</details>\n\n"
);
}
});
const docsContent = await readFile("./DOCS.md", "utf8");
await writeFile(
"./DOCS.md",
docsContent.slice(
0,
docsContent.indexOf("<!-- AUTOGENERATED DOCS AFTER THIS POINT -->")
) +
"<!-- AUTOGENERATED DOCS AFTER THIS POINT -->\n" +
(await Promise.all(packageDocs)).join("\n\n\n")
);
}

View File

@@ -1,23 +0,0 @@
import { genDocsMd } from "./genDocsMd";
export const manuallyIgnore = new Set(["CojsonInternalTypes"]);
async function main() {
await genDocsMd();
}
export function indent(text: string): string {
return text
.split("\n")
.map((line) => " " + line)
.join("\n");
}
export function indentEnd(text: string): string {
return text
.split("\n")
.map((line, i) => (i === 0 ? line : " " + line))
.join("\n");
}
main().catch(console.error);

11
homepage/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:18-slim
COPY node_modules ./node_modules
COPY homepage/.next/standalone ./homepage
COPY homepage/.next/static ./homepage/.next/static
EXPOSE 3001
ENV PORT 3001
CMD ["node", "homepage/server.js"]

View File

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View File

@@ -1,65 +0,0 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
# If using npm comment out above and use below instead
# RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -1,202 +0,0 @@
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Global Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz Global Mesh
<Slogan>Serverless sync & storage for Jazz apps.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
## The first Collaboration Delivery Network
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
### Free Tier
<span className="text-lg font-medium bg-emerald-200 dark:bg-emerald-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</span>
<p className="text-sm">Later, any usage under $1/mo will be free.</p>
<Grid>
<GridItem className="md:col-span-2">
### Unlimited <ComingSoonBadge/>
<div className="lg:text-2xl border rounded-lg px-1 py-3 text-center">${pricePer1MtxSyncedOut} <small>per 1M TXs synced out</small> + ${pricePer1MtxStored}<small>/mo per 1M TXs stored</small></div>
<p><small>$6/mo minimum usage</small></p>
A TX (transaction) represents an **individual user action**, or **up to 100KB of binary data**.
</GridItem>
<GridItem className="col-start-1">
#### Transactions synced out:
<div className="text-sm">
- Transactions sent out from Global Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each transaction should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Transactions stored:
<div className="text-sm">
- Transactions that are continuously persisted.
- Counted per second.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of transactions generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- 4 users co-editing 10 pages of text, typing them out as individual character inserts:
- 3,000 inserts/page &times; 10 pages = 30,000 transactions
- 30,000 transactions stored = ${30000 * pricePerTxStored} / mo
- 3 &times; 30,000 transactions synced out = ${3 * 30000 * pricePerTxSyncedOut} one-time
- 4 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 2h/day for a month
- 4 users &times; 10 FPS &times; 10s/min &times; 60min/h * 2h/day &times; 30days = 1.44M transactions
- 1.44M transactions stored = ${1440000 * pricePerTxStored} / mo = ${1440000 * pricePerTxStored / 4} / mo / user
- 3 &times; 1.44M transactions synced out = ${(3 * 1440000 * pricePerTxSyncedOut).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time = ${(3 * 1440000 * pricePerTxSyncedOut / 4).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time / user
- A livestreamer streaming video (1GB total) to 100 viewers (combined live & on-demand)
- 1GB = 10,000 transactions (100KB each)
- 10,000 transactions stored = ${10000 * pricePerTxStored} / mo (= ${10000 * 1000 * pricePerTxStored} per 1TB stored)
- 100 &times; 10,000 transaction synced out = ${100 * 10000 * pricePerTxSyncedOut} one-time (= ${10000 * 1000 * pricePerTxSyncedOut} per 1TB egress)
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan>You can rely on Global Mesh. But you don't have to.</Slogan>
<p>Because Jazz is open-source, you can optionally run your own sync nodes &mdash; in a variety of setups.</p>
<Grid>
<GridCard>
#### Global Mesh + Data Backup Node.
<p className="no-prose text-base">Connect your users to Global Mesh for all its benefits, but also run and connect your own data backup node (just in case.)</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Every transaction is additionally synced to your backup node and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Global Mesh + DIY Mesh.
<p className="no-prose text-base">Connect your users to Global Mesh, or your own nodes as a lower-performance fallback. The two networks stay in constant sync.</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Every transaction is additionally synced to your DIY mesh and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">Build your own network of sync and storage nodes.
Handle networking, security and backups yourself.</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -1,281 +0,0 @@
import {
Slogan,
Grid,
GridItem,
GridFeature,
GridCard,
MultiplayerIcon,
ResponsiveIframe,
ComingSoonBadge
} from "@/components/forMdx";
import {
JazzLogo
} from "@/components/logos";
import {
WorkflowIcon,
UploadCloudIcon,
PlaneIcon,
MonitorSmartphoneIcon,
GaugeIcon,
} from "lucide-react";
import {
DataModel_ts,
App_tsx,
ChatWindow_tsx,
} from "@/codeSamples/examples/chat/src";
import Link from "next/link";
# Instant sync
<Slogan>Go beyond request/response &mdash; ship modern apps with sync.</Slogan>
Jazz is an open-source toolkit for building apps with **sync** & **secure collaborative data.**
<h2 className="md:mt-24">Hard things are easy now</h2>
Jazz replaces APIs, DBs and message queues with **a single new abstraction: CoJSON**.
This means you get **built-in capabilities** that took best-in-class apps years to build:
<Grid className="-mt-2 gap-[1px] border rounded-xl overflow-hidden border-stone-200 dark:border-stone-800 shadow-sm bg-stone-200 dark:bg-stone-800 [&>*]:rounded-none [&>*]:border-none [&>*]:bg-stone-50 [&>*]:dark:bg-stone-950">
<GridFeature icon={<MonitorSmartphoneIcon />}>Cross-device sync</GridFeature>
<GridFeature icon={<MultiplayerIcon/>}>Real-time multiplayer</GridFeature>
<GridFeature icon={<WorkflowIcon />}>Automatic granular datafetching</GridFeature>
<GridFeature icon={<UploadCloudIcon />}>Local & cloud persistence</GridFeature>
<GridFeature icon={<PlaneIcon />}>Offline support & Quick reconnect</GridFeature>
<GridFeature icon={<GaugeIcon />}>Instant UI updates & quick loads</GridFeature>
</Grid>
<div className="-mx-[calc(min(0,(100vw-95rem)/2))]">
### First impressions
<Slogan small>A chat app in 82 lines of code.</Slogan>
<Grid className="mt-0">
<GridItem>
<DataModel_ts/>
</GridItem>
<GridItem className="md:col-start-1">
<App_tsx/>
</GridItem>
<GridItem className="md:col-start-2 md:row-start-1 md:row-span-2">
<ChatWindow_tsx/>
</GridItem>
<ResponsiveIframe src="https://chat.jazz.tools" className="lg:col-start-3 lg:row-start-1 lg:row-span-2 rounded-xl overflow-hidden min-h-[50vh]"/>
</Grid>
</div>
## CoJSON
<Slogan small>The collaborative core.</Slogan>
Jazz is built around **CoJSON,** a new abstraction for **sync** & **secure collaborative data.** And while it does all the heavy lifting...
- **multi-device co-editing**
- **user identities & accounts**
- **permissions** & **roles**
- **sync** & **caching**
- **persistence**
...its API couldn't be simpler: CoJSON makes collaboration and secure access control feel like **inherent properties of your data**.
### Collaborative Values
<Slogan small>Your new building blocks.</Slogan>
Collaborative Values (CoValues) **can be edited as if they were simple local data,** but they're **automatically encrypted, signed** and **synced** between participants.
CoValues also **keep their full edit history,** including author metadata and potential editing conflicts. This makes it **super simple to build collaborative and social features.**
<Grid className="lg:gap-y-8">
<GridCard>
### `CoMap`
<div className="text-sm">
- Collaborative key-value map
- Possible values:
- Immutable JSON & IDs of other CoValues
</div>
</GridCard>
<GridCard>
### `CoList`
<div className="text-sm">
- Collaborative ordered list
- Possible items:
- Immutable JSON & IDs of other CoValues
</div>
</GridCard>
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
The bread and butter of datastructures, with collaboration built-in. You can build whole apps with just these.
</GridItem>
<GridCard>
### `CoString` <ComingSoonBadge/>
<div className="text-sm">
- Collaborative plain-text
- Implemented as a CoList of unicode graphemes
- Supports concurrent inserts and deletes well
</div>
</GridCard>
<GridCard>
### `CoText` <ComingSoonBadge/>
<div className="text-sm">
- Collaborative rich-text based on `CoString` and a `CoMap` of collaborative markup ranges
- Gracefully prevents most editing conflicts
- Rendered as markdown, HTML, JSX, etc.
</div>
</GridCard>
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
A shocking amount of UI is text editing. CoJSON offers correct, versatile primitives.
</GridItem>
<GridCard>
### `CoStream`
<div className="text-sm">
- Collection of independent per-user items streams:
- Immutable JSON & IDs of other CoValues
- Great for presence, reactions, polls, replies etc.
</div>
</GridCard>
<GridCard>
### `BinaryCoStream`
<div className="text-sm">
- A `CoStream` of binary data chunks
- Use for files and media streams
- Create, load, sync and store binary blobs or live-streams as just another kind of object
</div>
</GridCard>
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
Two extra tools that let you do everything you need in your app without having to integrate additional external services.
</GridItem>
</Grid>
### Groups & Accounts
<Slogan small>First-class user identities & secure permissions.</Slogan>
<Grid>
<GridCard>
### `Group`
<div className="text-sm">
- A scope where specified accounts have roles (`reader`/`writer`/`admin`).
- A `Group` owns `CoValues`, with access right determined by group roles.
- Accounts can be added to groups directly or using shareable invite secrets.
</div>
</GridCard>
<GridCard>
### `Account`
<div className="text-sm">
- Represents a single user and their signing/encryption keys.
- Has a private account root and a public profile
- Can contain arbitrary app-specific data
</div>
</GridCard>
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
A simple API to define access control from anywhere, verifiably enforced by encryption and signatures.
</GridItem>
</Grid>
## The Jazz Toolkit
<Slogan small>Idiomatic bindings for CoJSON, with batteries included.</Slogan>
Supported environments:
<div className="text-sm">
- Browser (sync via WebSockets, IndexedDB persistence)
- React
- Vanilla JS / framework agnostic base
- React Native <ComingSoonBadge/>
- NodeJS (sync via WebSockets, SQLite persistence) <ComingSoonBadge/>
- Swift, Kotlin, Rust <ComingSoonBadge when="later"/>
</div>
<Grid>
<GridCard>
### Auto-sub
<Slogan small>Let your UI drive data-syncing.</Slogan>
<div className="text-sm">
- Load and auto-subscribe to deeply nested `CoValues` with a reactive hook (or callback).
- Access properties & metadata as plain JSON.
- Make granular changes with simple mutators.
- No queries needed, everything loads on-demand: <br/>
`profile?.tweets?.map(tweet => tweet?.text)`
</div>
</GridCard>
<GridCard>
### Cursors & carets
<Slogan small>Ready-made spatial presence.</Slogan>
<div className="text-sm">
- 2D canvas cursors <ComingSoonBadge/>
- Text carets <ComingSoonBadge/>
- Element-based focus-presence <ComingSoonBadge/>
- Scroll-based / out-of-bounds helpers <ComingSoonBadge/>
</div>
</GridCard>
<GridCard>
### Auth providers
<Slogan small>Plug and play different kinds of auth.</Slogan>
<div className="text-sm">
- DemoAuth (for quick multi-user demos)
- WebAuthN (TouchID/FaceID)
- Auth0, Clerk & Okta <ComingSoonBadge/>
- NextAuth <ComingSoonBadge/>
</div>
</GridCard>
<GridCard>
### Two-way sync to your DB
<Slogan small>Add Jazz to an existing app.</Slogan>
<div className="text-sm">
- Prisma <ComingSoonBadge/>
- Drizzle <ComingSoonBadge/>
- PostgreSQL introspection <ComingSoonBadge/>
</div>
</GridCard>
<GridCard>
### File upload & download
<Slogan small>Just use `<input type="file"/>`.</Slogan>
<div className="text-sm">
- Easily convert from and to Browser `Blob`s
- Super simple progressive image loading
</div>
</GridCard>
<GridCard>
### Video presence & calls
<Slogan small>Stream and record audio & video.</Slogan>
<div className="text-sm">
- Automatic WebRTC connections between `Group` members <ComingSoonBadge/>
- Audio/video recording into `BinaryCoStreams` <ComingSoonBadge/>
</div>
</GridCard>
</Grid>
## Global Mesh
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
To give you sync & secure collaborative data instantly on a global scale, we're running Global Mesh. It works with any CoJSON-based app, requires no setup and has straightforward, scale-to-zero pricing.
Global Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
<Link href="/mesh" target="_blank">Learn more about Global Mesh</Link>
## Get Started
- See the <Link href="https://github.com/gardencmp/jazz#todo-list" target="_blank">Todo List Example Walkthrough</Link>
- <Link href="https://github.com/gardencmp/jazz/blob/main/DOCS.md" target="_blank">Read the docs</Link>
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">Join our Discord</Link>

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "stone",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -1,85 +0,0 @@
import { ComingSoonBadge, Grid, GridCard, GridItem } from "./forMdx";
export const pricePer1MtxSyncedOut = 1;
export const pricePer1MtxStored = 2;
export const pricePerTxSyncedOut = pricePer1MtxSyncedOut / 1_000_000;
export const pricePerTxStored = pricePer1MtxStored / 1_000_000;
export function Pricing() {
const worstCaseBytesPerTx = 200_000;
const avgCaseBytesPerTx = 10_000;
const worstCaseCostPerTBstorage = 20;
const worstCaseCostPerTxStored =
worstCaseBytesPerTx * (worstCaseCostPerTBstorage / 1_000_000_000_000);
const avgCaseCostPerTxStored =
avgCaseBytesPerTx * (worstCaseCostPerTBstorage / 1_000_000_000_000);
const costPerTBEgress = 5;
const serverCost = 30;
const txOutPerSecondPerServer = 100;
const txPerMonthPerServer = txOutPerSecondPerServer * 60 * 60 * 24 * 30;
const worstCaseCostPerTxSyncedOut =
worstCaseBytesPerTx * (costPerTBEgress / 1_000_000_000_000) +
serverCost / txPerMonthPerServer;
const avgCaseCostPerTxSyncedOut =
avgCaseBytesPerTx * (costPerTBEgress / 1_000_000_000_000) +
serverCost / txPerMonthPerServer;
const recommendedSyncToStorageRatio = 0.2;
const freeTierSyncedOut = 100_000;
const freeTierStored = freeTierSyncedOut / recommendedSyncToStorageRatio;
const proTierSyncedOut = 500_000;
const proTierStored = proTierSyncedOut / recommendedSyncToStorageRatio;
return (
<Grid>
<GridCard>
<h3>Free Tier</h3>
<p className="text-lg font-medium bg-indigo-200 dark:bg-indigo-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</p>
<p className="text-sm">Later, any usage under $2/mo will be free.</p>
</GridCard>
<GridCard>
<h3>Unlimited <ComingSoonBadge/></h3>
<p className="text-lg">
{fmt$(pricePer1MtxSyncedOut)} per 1,000,000 transactions
synced out
{/* <br />
Avg cost: {fmt$(avgCaseCostPerTxSyncedOut * 1_000_000)}
<br />
Worst cost: {fmt$(worstCaseCostPerTxSyncedOut * 1_000_000)} */}
<br/>
{fmt$(pricePer1MtxStored)}
<small>/mo</small> per 1,000,000 transactions stored
{/* <br />
Avg cost: {fmt$(avgCaseCostPerTxStored * 1_000_000)}
<br />
Worst cost: {fmt$(worstCaseCostPerTxStored * 1_000_000)} */}
</p>
<p className="text-sm">See below for how transactions are defined.</p>
</GridCard>
<GridCard>
<h3>Enterprise</h3>
<p className="text-sm">Custom deployment in the cloud, your private cloud, on-premises or hybrids?</p>
<p className="text-sm">SLAs and dedicated support? White-glove integration services?</p>
</GridCard>
</Grid>
);
}
function fmt(num: number) {
return num.toLocaleString("en-US", {});
}
function fmt$(num: number) {
return (
"$" +
num.toLocaleString("en-US", {
maximumSignificantDigits: 3,
})
);
}

View File

@@ -1,6 +0,0 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,7 +0,0 @@
import type { MDXComponents } from 'mdx/types'
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
}
}

View File

@@ -1,26 +0,0 @@
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure `pageExtensions`` to include MDX files
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
// Optionally, add any other Next.js config below
experimental: {
serverActions: true,
},
};
const withMDX = createMDX({
// Add markdown plugins here, as desired
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});
const config = {
...withMDX(nextConfig),
output: 'standalone'
};
export default config;

View File

@@ -1,42 +0,0 @@
{
"name": "homepage-jazz",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@evilmartians/harmony": "^1.0.0",
"@icons-pack/react-simple-icons": "^9.1.0",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.5.4",
"@types/mdx": "^2.0.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.284.0",
"next": "13.5.4",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"shiki-twoslash": "^3.1.2",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@csstools/postcss-oklab-function": "^3.0.6",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.4",
"postcss": "^8",
"tailwindcss": "^3",
"typescript": "^5"
}
}

View File

@@ -1,7 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
'@csstools/postcss-oklab-function': { 'preserve': true },
autoprefixer: {},
},
}

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier"]
}

View File

@@ -33,3 +33,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
typedoc
codeSamples

View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

View File

@@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -0,0 +1,375 @@
import { Slogan } from "@/components/forMdx";
import { JazzLogo } from "@/components/logos";
<h1 id="guide">Learn some Jazz.</h1>
<Slogan>Build an issue tracking app with distributed state.</Slogan>
Our issues app will be quite simple, but it will have team collaboration. <span className="text-nowrap">**Let's call it... &ldquo;Circular.&rdquo;**</span>
We'll build everything **step-by-step,** in typical, immediately usable stages. We'll explore many important things Jazz does &mdash; so **follow along** or **just pick things out.**
<h2 id="setup">Project Setup</h2>
1. Create a project from a generic Vite starter template:
```bash
npx degit gardencmp/vite-ts-react-tailwind circular
cd circular
npm install
npm run dev
```
You should now have an empty app running, typically at [localhost:5173](http://localhost:5173).<br/>
<small>
(If you make changes to the code, the app will automatically refresh.)
</small>
2. Install `jazz-tools` and `jazz-react`<br/>
<small>(in a new terminal window):</small>
```bash
cd circular
npm install jazz-tools jazz-react
```
3. Set up a Jazz context, by modifying `src/main.tsx`:
```tsx subtle=1,2,3,4,13,15,16,17,19
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { JazzReact } from "jazz-react";
const Jazz = createJazzReactContext({
auth: DemoAuth({ appName: "Circular" }),
peer: "wss://mesh.jazz.tools/?key=you@example.com", // <- put your email here to receive a proper API key for later
});
export const { useAccount, useCoState } = Jazz;
ReactDOM.createRoot(document.getElementById("root")!).render(
<Jazz.Provider>
<React.StrictMode>
<App />
</React.StrictMode>
</Jazz.Provider>,
);
```
This sets Jazz up, extracts app-specific hooks for later, and wraps our app in the provider.
TODO: explain Auth
<h2 id="intro-to-covalues">Intro to CoValues</h2>
Let's learn about the **central idea** behind Jazz: **Collaborative Values.**
What if we could **treat distributed state like local state?** That's what CoValues do.
We can
- **create** CoValues, anywhere
- **load** CoValues by `ID`, from anywhere else
- **edit** CoValues, from anywhere, by mutating them like local state
- **subscribe to edits** in CoValues, whether they're local or remote
<h3 id="first-schema">Declaring our own CoValues</h3>
To make our own CoValues, we first need to declare a schema for them. Think of a schema as a combination of TypeScript types and runtime type information.
Let's start by defining a schema for our most central entity in Circular: an **Issue.**
Create a new file `src/schema.ts` and add the following:
```ts
import { CoMap, co } from "jazz-tools";
export class Issue extends CoMap {
title = co.string;
description = co.string;
estimate = co.number;
status? = co.literal("backlog", "in progress", "done");
}
```
TODO: explain what's happening
<h3>Reading from CoValues</h3>
CoValues are designed to be read like simple local JSON state. Let's see how we can read from an Issue by building a component to render one.
Create a new file `src/components/Issue.tsx` and add the following:
```tsx
import { Issue } from "../schema";
export function IssueComponent({ issue }, { issue: Issue }) {
return (
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<h2>{issue.title}</h2>
<p className="col-span-3">{issue.description}</p>
<p>Estimate: {issue.estimate}</p>
<p>Status: {issue.status}</p>
</div>
);
}
```
Simple enough!
<h3>Creating CoValues</h3>
To actually see an Issue, we have to create one. This is where things start to get interesting...
Let's modify `src/App.tsx` to prepare for creating an Issue and then rendering it:
```tsx subtle=5,13,14,15
import { useState, useCallback } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
function App() {
const [issue, setIssue] = useState<Issue>();
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button>Create Issue</button>;
}
}
export default App;
```
Now, finally, let's implement creating an issue:
```tsx subtle=1,2,3,5,6,8,23,24,25,27,28,29,30
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
import { useAccount } from "./main";
function App() {
const { me } = useAccount();
const [issue, setIssue] = useState<Issue>();
const createIssue = () => {
const newIssue = Issue.create(
{
title: "Buy terrarium",
description: "Make sure it's big enough for 10 snails.",
estimate: 5,
status: "backlog",
},
{ owner: me },
);
setIssue(newIssue);
};
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button onClick={createIssue}>Create Issue</button>;
}
}
export default App;
```
Now you should be able to create a new issue by clicking the button and then see it rendered.
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider">
Preview
</div>
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<h2>Buy terrarium</h2>
<p className="col-span-3">Make sure it's big enough for 10 snails.</p>
<p>Estimate: 5</p>
<p>Status: backlog</p>
</div>
</div>
We'll already notice one interesting thing here:
- We have to create every CoValue with an `owner`!
- this will determine access rights on the CoValue, which we'll learn about in "Groups & Permissions"
- here we set `owner` to the current user `me`, which we get from the Jazz context / `useAccount`
**Behind the scenes, Jazz not only creates the Issue in memory but also automatically syncs an encrypted version to the cloud and persists it locally. The Issue also has a globally unique ID.**
We'll make use of both of these facts in a bit, but for now let's start with local editing and subscribing.
<h3>Editing CoValues and subscribing to edits</h3>
Since we're the owner of the CoValue, we should be able to edit it, right?
And since this is a React app, it would be nice to subscribe to edits of the CoValue and reactively re-render the UI, like we can with local state.
This is exactly what the `useCoState` hook is for!
- Note that `useCoState` doesn't take a CoValue directly, but rather a CoValue's schema, plus its `ID`.
- So we'll slightly adapt our `useState` to only keep track of an issue ID...
- ...and then use `useCoState` to get the actual issue
Let's modify `src/App.tsx`:
```tsx subtle=1,2,3,4,5,6,7,12,13,14,15,16,17,18,19,20,21,23,25,26,27,28,29,30,32
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
import { useAccount } from "./main";
function App() {
const { me } = useAccount();
const [issueID, setIssueID] = useState<ID<Issue>>();
const issue = useCoState(Issue, issueID);
const createIssue = () => {
const newIssue = Issue.create(
{
title: "Buy terrarium",
description: "Make sure it's big enough for 10 snails.",
estimate: 5,
status: "backlog",
},
{ owner: me },
);
setIssueID(newIssue.id);
};
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button onClick={createIssue}>Create Issue</button>;
}
}
export default App;
```
And now for the exciting part! Let's make `src/components/Issue.tsx` an editing component.
```tsx subtle=1,3,4,5,28,29,30,31
import { Issue } from "../schema";
export function IssueComponent({ issue }, { issue: Issue }) {
return (
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<input type="text"
value={issue.title}
onChange={(event) => { issue.title = event.target.value }}/>
<textarea className="col-span-3"
value={issue.description}
onChange={(event) => { issue.description = event.target.value }}/>
<label className="flex">
Estimate:
<input type="number" className="text-right min-w-0"
value={issue.estimate}
onChange={(event) => { issue.estimate = Number(event.target.value) }}/>
</label>
<select
value={issue.status}
onChange={(event) => {
issue.status = event.target.value as "backlog" | "in progress" | "done"
}}
>
<option value="backlog">Backlog</option>
<option value="in progress">In Progress</option>
<option value="done">Done</options>
</select>
</div>
);
}
```
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider">
Preview
</div>
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<input type="text" value={"Buy terrarium"} />
<input
type="text"
className="col-span-3"
value={"Make sure it's big enough for 10 snails."}
/>
<label className="flex">
Estimate:{" "}
<input type="number" value={5} className="text-right min-w-0" />
</label>
<select value={"backlog"}>
<option value="backlog">Backlog</option>
<option value="in progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
</div>
You'll immediately notice that we're doing something non-idiomatic for React: we mutate the issue directly, by assigning to its properties.
This works because CoValues intercept these edits, update their local view accordingly (React doesn't really care after rendering) and then notify subscribers of the change, who will receive a fresh, updated view of the CoValue.
<aside className="text-sm border rounded px-4 my-4 max-w-3xl [&_pre]:mx-0">
<h4 className="not-prose text-base py-2 mb-3 -mx-4 px-4 border-b">💡 A Quick Overview of Subscribing to CoValues</h4>
There are three main ways to subscribe to a CoValue:
1. Directly on an instance:
```ts
const unsub = issue.subscribe((updatedIssue) => console.log(updatedIssue));
```
2. If you only have an ID (this will load the issue if needed):
```ts
const unsub = Issue.subscribe(issueID, { as: me }, (updatedIssue) => {
console.log(updatedIssue);
});
```
3. If you're in a React component, to re-render reactively:
`tsx
const issue = useCoState(Issue, issueID);
`
By the way, `useCoState` is basically just an optimized version of
```ts
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
const { me } = useAccount();
const [value, setValue] = useState<V>();
useEffect(() => Schema.subscribe(id, { as: me }, setValue), [id]);
return value;
}
```
</aside>
We have one subscriber on our Issue, with `useCoState` in `src/App.tsx`, which will cause the `App` component and its children to re-render whenever the Issue changes.
<h3>Automatic local & cloud persistence</h3>
So far our Issue CoValues just looked like ephemeral local state. We'll now start exploring the first main feature that makes CoValues special: **automatic persistence.**
<span className="text-amber-500">
🚧 OH NO - This is as far as we've written the Guide. 🚧
</span>
{" -> "}
<a href="https://github.com/gardencmp/jazz/issues/186">Complain on GitHub</a>
<h3>Remote sync</h3>
<h2 id="refs-and-on-demand-subscribe">Refs & Auto-Subscribe</h2>
<h2 id="groups-and-permissions">Groups & Permissions</h2>
<h2 id="accounts-and-migrations">Accounts & Migrations</h2>
<h2 id="backend-workers">Backend Workers</h2>

View File

@@ -0,0 +1,55 @@
import { DocNav } from "@/components/docs/nav";
import { PackageDocs } from "@/components/docs/packageDocs";
import Guide from "./guide.mdx";
export const metadata = {
title: "jazz - Docs",
description: "Jazz Guide, FAQ & Docs.",
};
export default function Page() {
return (
<>
<div className="hidden md:block bg-stone-100 dark:bg-stone-900 p-4 rounded-xl sticky overflow-y-scroll overscroll-contain w-[16rem] h-[calc(100dvh-8rem)] -mb-[calc(100dvh-8rem)] top-[6rem] mr-10 prose-sm prose-ul:pl-1 prose-ul:ml-1 prose-li:my-2 prose-li:leading-tight prose-ul:list-['-']">
<DocNav />
</div>
<div className="md:ml-[20rem]">
<Guide />
<h1 id="faq">FAQ</h1>
<p>
<span className="text-amber-500">
🚧 OH NO - We don&apos;t have any FAQ yet. 🚧
</span>{" "}
{"->"}{" "}
<a href="https://github.com/gardencmp/jazz/issues/187">
Complain on GitHub
</a>
</p>
<div className="xl:-mr-[calc(50vw-40rem)]">
<h1>API Reference</h1>
<p>
<span className="text-amber-500">
🚧 OH NO - These docs are still highly
work-in-progress. 🚧
</span>{" "}
{"->"}{" "}
<a href="https://github.com/gardencmp/jazz/issues/188">
Complain on GitHub
</a>
</p>
<PackageDocs package="jazz-tools" />
<PackageDocs package="jazz-react" />
<PackageDocs package="jazz-browser" />
<PackageDocs package="jazz-browser-media-images" />
<PackageDocs package="jazz-nodejs" />
</div>
</div>
</>
);
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -99,7 +99,7 @@ pre.shiki div.highlight {
background-color: #f1f8ff;
}
pre.shiki div.line {
min-height: 1rem;
/* min-height: 1rem; */
counter-increment: lineNumber 1;
}
@@ -110,7 +110,12 @@ pre.shiki div.line::before {
width: 1.3rem;
padding-right: 0.3rem;
text-align: right;
@apply text-stone-200 dark:text-stone-800 text-[0.65rem];
transition: color 0.3s;
@apply text-stone-200/50 dark:text-stone-900 text-[0.65rem];
}
pre.shiki div.line:hover::before {
@apply text-stone-400 dark:text-stone-600;
}
/** Don't show the language identifiers */
@@ -147,7 +152,7 @@ pre.twoslash data-lsp:hover::before {
}
.shiki-outer {
@apply shadow-sm rounded-xl border border-stone-200 dark:border-stone-800;
@apply shadow-sm rounded-xl;
}
.shiki-filename {
@@ -156,7 +161,7 @@ pre.twoslash data-lsp:hover::before {
pre .code-container {
overflow: scroll;
@apply p-2 pl-0 bg-stone-50 dark:bg-stone-950 rounded-b-xl text-[0.8rem] leading-4;
@apply p-2 pl-0 bg-stone-75 dark:bg-stone-925 rounded-b-xl text-[0.8rem] leading-tight border border-stone-100 dark:border-stone-900;
}
/* The try button */
pre .code-container > a {
@@ -324,7 +329,12 @@ data-lsp {
}
.tag-container .twoslash-annotation {
position: absolute;
font-family: "JetBrains Mono", Menlo, Monaco, Consolas, Courier New,
font-family:
"JetBrains Mono",
Menlo,
Monaco,
Consolas,
Courier New,
monospace;
right: -10px;
/** Default annotation text to 200px */

View File

@@ -10,10 +10,19 @@ import { GcmpLogo, JazzLogo } from "@/components/logos";
import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
import { MailIcon } from "lucide-react";
import { DocNav } from "@/components/docs/nav";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope" });
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
const pragmata = localFont({
src: "../fonts/ppr_0829.woff2",
variable: "--font-ppr",
@@ -46,14 +55,13 @@ export default function RootLayout({
disableTransitionOnChange
>
<Nav
mainLogo={<JazzLogo className="w-24" />}
mainLogo={<JazzLogo className="w-24 -ml-2" />}
items={[
{ title: "Toolkit", href: "/" },
{ title: "Global Mesh", href: "/mesh" },
{ title: "Home", href: "/" },
{ title: "Sync & Storage Mesh", href: "/mesh" },
{
title: "Docs & Guides",
href: "https://github.com/gardencmp/jazz/blob/main/DOCS.md",
newTab: true,
title: "Docs",
href: "/docs",
},
{
title: "Blog",
@@ -90,11 +98,11 @@ export default function RootLayout({
icon: <SiTwitter className="w-5" />,
},
]}
docNav={<DocNav />}
/>
<main className="flex min-h-screen flex-col p-8 max-w-[80rem] w-full">
<main className="flex min-h-screen flex-col p-8 max-w-[80rem] w-full [&_*]:scroll-mt-[6rem]">
<article
className={[
"pt-20",
"prose lg:prose-lg max-w-none prose-stone dark:prose-invert",
"prose-headings:font-display",
"prose-h1:text-5xl lg:prose-h1:text-6xl prose-h1:font-medium prose-h1:tracking-tighter",
@@ -102,12 +110,15 @@ export default function RootLayout({
"prose-p:max-w-3xl prose-p:leading-snug",
"prose-strong:font-medium",
"prose-code:font-normal prose-code:leading-tight prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:-my-1 prose-code:rounded",
"prose-pre:max-w-3xl prose-pre:text-[0.8em] prose-pre:leading-[1.3] prose-pre:-mt-4 prose-pre:my-4 prose-pre:px-3 prose-pre:py-2 md:prose-pre:-mx-3 prose-pre:bg-stone-100 dark:prose-pre:bg-stone-900",
"prose-inner-code:font-normal prose-inner-code:text-[1em]",
].join(" ")}
>
{children}
</article>
</main>
<footer className="flex mt-10 min-h-[15rem] -mb-20 bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
<footer className="flex z-10 mt-10 min-h-[15rem] -mb-20 bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
<div className="p-8 max-w-[80rem] w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
<GcmpLogo monochrome className="w-32" />
@@ -129,14 +140,13 @@ export default function RootLayout({
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="/mesh"
>
Global Mesh
Sync & Storage Mesh
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/gardencmp/jazz/blob/main/DOCS.md"
newTab
href="/docs"
>
Docs & Guides
Docs
</NavLink>
</div>
{/* <div className="flex flex-col gap-2 text-sm">
@@ -192,14 +202,17 @@ export default function RootLayout({
</div>
<div className="col-span-3 md:col-start-2 lg:col-start-auto flex flex-col gap-2 text-sm">
Sign up for updates:
<Newsletter/>
<Newsletter />
</div>
</div>
</footer>
</ThemeProvider>
<script defer data-api="/api/event" data-domain="jazz.tools" src="/js/script.js"></script>
<script
defer
data-api="/api/event"
data-domain="jazz.tools"
src="/js/script.js"
></script>
</body>
</html>
);

View File

@@ -0,0 +1,253 @@
import {
Slogan,
Grid,
GridCard,
GridItem,
ComingSoonBadge,
} from "@/components/forMdx";
export const metadata = {
title: "jazz - Jazz Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
<div className="md:pt-20" />
# Sync & Storage Mesh
<Slogan>The first Collaboration Delivery Network.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
<Grid>
<GridCard>
### Mesh Free
<span className="text-2xl">$0</span>
- Best-effort sync
- 3,000 sync-minutes/mo
- 1 GB storage
{/* - Egress/mo: 5 million ops <span className="text-xs">or 50GB blobs</span> */}
{/* - Storage: 2.5 million ops <span className="text-xs">or 25GB blobs</span> */}
</GridCard>
<GridCard>
### Mesh Starter <ComingSoonBadge/>
<span className="text-2xl">$9</span>/mo
- Base-priority sync
- 6,000 sync-minutes/mo
- 100 GB storage
<div className="text-xs">
Extra usage:
- $9 per additional 6,000 sync-minutes
- $9 per additional 1TB storage/mo
</div>
</GridCard>
<GridCard>
### Mesh Pro <ComingSoonBadge/>
<span className="text-2xl">$79</span>/mo
- High-priority sync
- 30,000 sync-minutes/mo
- 1 TB storage
- Offer sync.yourdomain.com
<div className="text-xs">
Extra usage:
- $15 per additional 6,000 sync-minutes
- $15 per additional 1TB storage/mo
</div>
</GridCard>
{/*<GridCard>
### Mesh Enterprise <ComingSoonBadge/>
<span className="text-2xl">Custom</span>
- Custom SLA
- Custom cloud deployment
- Dedicated support
- Audit logs
</GridCard>*/}
</Grid>
### FAQ:
<div className="text-sm">
#### How are sync-minutes counted?
Sync-minutes are counted on a **per-connected-device, per-minute basis.**<br/>
A device is considered syncing **only when it's actively sending or receiving data.**
#### How can I estimate my usage?
The best way to estimate your usage is to **guess how many minutes per month each user will spend actively using your app.**
Storage is mostly determined by large binary blobs (like images or videos) that you store in Jazz.
#### What happens if I exceed my plan's limits?
All limits are initially soft limits, so don't worry if you suddenly get lots of users or traffic!
Sync beyond the limit is still served, but at a lower priority.
Data beyond the storage limit is still stored and backed up, but may be significantly slower to access.
If you exceed your plan's limits consistently, we'll reach out to discuss upgrading your plan.
</div>
{/* *### Examples:* */}
<div className="text-sm">
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan small>You can rely on Jazz Mesh. But you don't have to.</Slogan>
<p>
Because Jazz is open-source, you can optionally run your own sync nodes
&mdash; in a variety of setups.
</p>
<Grid>
<GridCard>
#### Jazz Mesh + Data Backup Node.
<p className="no-prose text-base">
Connect your users to Jazz Mesh for all its benefits, but also run and
connect your own data backup node (just in case.)
</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Your backup node is counted as a continously connected device.
</div>
</GridCard>
<GridCard>
#### Jazz Mesh + DIY Mesh.
<p className="no-prose text-base">
Connect your users to Jazz Mesh, or your own nodes as a lower-performance
fallback. The two networks stay in constant sync.
</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Any of your sync nodes connected to Jazz Mesh is counted as a continously connected device.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">
Build your own network of sync and storage nodes. Handle networking,
security and backups yourself.
</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -0,0 +1,272 @@
import {
Slogan,
Grid,
GridItem,
GridFeature,
GridCard,
MultiplayerIcon,
ResponsiveIframe,
ComingSoonBadge,
} from "@/components/forMdx";
import { JazzLogo } from "@/components/logos";
import {
WorkflowIcon,
UploadCloudIcon,
PlaneIcon,
MonitorSmartphoneIcon,
GaugeIcon,
UsersIcon,
FileLock2Icon,
HardDriveDownloadIcon,
} from "lucide-react";
import { App_tsx, ChatScreen_tsx } from "@/codeSamples/examples/chat/src";
import Link from "next/link";
<div className="md:pt-20" />
# Instant sync.
<Slogan>A new way to build apps with distributed state.</Slogan>
<Grid className="gap-[1px] -mx-4 md:-mx-6 rounded-xl overflow-hidden bg-stone-50 dark:bg-stone-950 [&>*]:rounded-none [&>*]:border-none [&>*]:bg-stone-100 [&>*]:dark:bg-stone-900">
<GridFeature icon={<MonitorSmartphoneIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Cross-device sync</GridFeature>
<GridFeature icon={<MultiplayerIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Real-time multiplayer</GridFeature>
<GridFeature icon={<UsersIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Team/social features</GridFeature>
<GridFeature icon={<FileLock2Icon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Precise permissions</GridFeature>
{/* <GridFeature icon={<WorkflowIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>On-demand granular datafetching</GridFeature> */}
<GridFeature icon={<UploadCloudIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Cloud sync & storage</GridFeature>
<GridFeature icon={<HardDriveDownloadIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>On-device storage</GridFeature>
<GridFeature icon={<PlaneIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Full offline support</GridFeature>
<GridFeature icon={<GaugeIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Instant UI updates</GridFeature>
<div className="col-start-1 row-start-1 row-span-2 col-span-2 px-4 md:px-6 text-base">
<h2 className="not-prose text-2xl font-medium tracking-tight mt-3 md:mt-5 mb-6">Hard things are easy now.</h2>
Jazz is an **open-source toolkit** that replaces APIs, databases and message queues with **a&nbsp;single new abstraction:**
**&ldquo;Collaborative Values&rdquo;** &mdash; **distributed state** with **secure permissions built-in.**
Features that used to take months to build now work out-of-the-box.
</div>
</Grid>
<div className="-mx-[calc(min(0,(100vw-95rem)/2))]">
### First impressions&hellip;
<Slogan small>A chat app in 84 lines of code.</Slogan>
<Grid className="mt-0 -mx-4 md:-mx-6">
<GridItem className="md:col-start-1 col-span-2">
<App_tsx/>
</GridItem>
<GridItem className="md:col-start-3 col-span-2">
<ChatScreen_tsx/>
</GridItem>
<ResponsiveIframe src="https://chat.jazz.tools" localSrc="http://localhost:5173" className="lg:col-start-5 col-span-2 rounded-xl overflow-hidden min-h-[50vh]"/>
</Grid>
</div>
## Collaborative Values
<Slogan small>Your new building blocks.</Slogan>
<div className='text-base'>
Based on CRDTs and public-key cryptography, CoValues...
- Can be read & edited like simple local JSON state
- Can be created anywhere, are automatically synced & persisted
- Always keep full edit history & author metadata
- Automatically resolve most conflicts
</div>
### Bread-and-butter datastructures
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
<GridCard>
#### `CoMap`
<div className="text-sm">
- Collaborative key-value map - Possible values: - Immutable JSON &
other CoValues
</div>
</GridCard>
<GridCard>
#### `CoList`
<div className="text-sm">
- Collaborative ordered list - Possible items: - Immutable JSON & other
CoValues
</div>
</GridCard>
<GridCard>
#### `CoPlainText` & `CoRichText` <ComingSoonBadge />
<div className="text-sm">
- Collaborative plain-text & rich-text - Gracefully prevents most
editing conflicts - Rendered as markdown, HTML, JSX, etc.
</div>
</GridCard>
<GridCard>
#### `CoStream`
<div className="text-sm">
- Collection of independent per-user items streams:
- Immutable JSON & other CoValues
- Great for presence, reactions, polls, replies etc.
</div>
</GridCard>
</Grid>
### First-class files & binary data
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
<GridCard>
#### `BinaryCoStream`
<div className="text-sm">
- Represents a file or live binary stream - Can be referenced and synced
like any other CoValue - Can easily be converted from/to browser `Blob`s
- <code>{`<input type="file"/>`}</code> -> `BinaryCoStream` -> `Blob` ->
`BlobURL`
</div>
</GridCard>
<GridCard>
#### `ImageDefinition`
<div className="text-sm">
- Represents multiple resolutions of the same image - Can be
progressively loaded, including super fast blur preview & image size
info
</div>
</GridCard>
</Grid>
### Secure permissions, authorship & teams
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
<GridCard>
#### `Group`
<div className="text-sm">
- A scope where specified accounts have roles (`reader`/`writer`/`admin`).
- A `Group` owns `CoValues`, with access right determined by group roles.
- Accounts can be added to groups directly or using shareable invite secrets.
</div>
</GridCard>
<GridCard>
#### `Account`
<div className="text-sm">
- Represents a single user and their signing/encryption keys.
- Has a private account root and a public profile
- Can contain arbitrary app-specific data
</div>
</GridCard>
</Grid>
## The Jazz Toolkit
<Slogan small>A high-level toolkit for building apps around CoValues.</Slogan>
Supported environments:
<div className="text-sm">
- Browser (sync via WebSockets, IndexedDB persistence)
- React
- Vanilla JS / framework agnostic base
- React Native <ComingSoonBadge/>
- NodeJS (sync via WebSockets, SQLite persistence) <ComingSoonBadge/>
- Swift, Kotlin, Rust <ComingSoonBadge when="later"/>
</div>
<Grid>
<GridCard>
### Auto-sub
<Slogan small>Let your UI drive data-syncing.</Slogan>
<div className="text-sm">
- Load and auto-subscribe to deeply nested `CoValues` with a reactive
hook (or callback). - Access properties & metadata as plain JSON. - Make
granular changes with simple mutators. - No queries needed, everything
loads on-demand: <br />
`profile?.tweets?.map(tweet => tweet?.text)`
</div>
</GridCard>
<GridCard>
### Cursors & carets
<Slogan small>Ready-made spatial presence.</Slogan>
<div className="text-sm">
- 2D canvas cursors <ComingSoonBadge />
- Text carets <ComingSoonBadge />
- Element-based focus-presence <ComingSoonBadge />
- Scroll-based / out-of-bounds helpers <ComingSoonBadge />
</div>
</GridCard>
<GridCard>
### Auth Providers
<Slogan small>Plug and play different kinds of auth.</Slogan>
<div className="text-sm">
- DemoAuth (for quick multi-user demos)
- WebAuthN (TouchID/FaceID)
- Auth0, Clerk & Okta <ComingSoonBadge/>
- NextAuth <ComingSoonBadge/>
</div>
</GridCard>
<GridCard>
### Two-way sync to your DB
<Slogan small>Add Jazz to an existing app.</Slogan>
<div className="text-sm">
- Prisma <ComingSoonBadge />
- Drizzle <ComingSoonBadge />
- PostgreSQL introspection <ComingSoonBadge />
</div>
</GridCard>
<GridCard>
### File upload & download
<Slogan small>Just use `<input type="file"/>`.</Slogan>
<div className="text-sm">
- Easily convert from and to Browser `Blob`s
- Super simple progressive image loading
</div>
</GridCard>
<GridCard>
### Video presence & calls
<Slogan small>Stream and record audio & video.</Slogan>
<div className="text-sm">
- Automatic WebRTC connections between `Group` members <ComingSoonBadge/>
- Audio/video recording into `BinaryCoStreams` <ComingSoonBadge/>
</div>
</GridCard>
</Grid>
## Jazz Mesh
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
To give you sync and secure collaborative data instantly on a global scale, we're running Jazz Mesh. It works with any Jazz-based app, requires no setup and has straightforward, scale-to-zero pricing.
Jazz Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
<Link href="/mesh" target="_blank">
Learn more about Jazz Mesh
</Link>
## Get Started
- <Link href="/docs" target="_blank">
Read the docs
</Link>
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">
Join our Discord
</Link>

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "stone",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -3,17 +3,18 @@
import { useLayoutEffect, useState, useRef, IframeHTMLAttributes } from "react";
export function ResponsiveIframe(
props: IframeHTMLAttributes<HTMLIFrameElement>
props: IframeHTMLAttributes<HTMLIFrameElement> & { localSrc: string },
) {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [url, setUrl] = useState<string | undefined>(props.src);
const [url, setUrl] = useState<string | undefined>();
const [src, setSrc] = useState<string | undefined>();
useLayoutEffect(() => {
const listener = (e: MessageEvent) => {
console.log(e);
if (e.data.type === "navigate" && props.src?.startsWith(e.origin)) {
if (e.data.type === "navigate" && src?.startsWith(e.origin)) {
setUrl(e.data.url);
}
};
@@ -21,7 +22,7 @@ export function ResponsiveIframe(
return () => {
window.removeEventListener("message", listener);
};
}, [props.src]);
}, [src]);
useLayoutEffect(() => {
if (!containerRef.current) return;
@@ -38,10 +39,23 @@ export function ResponsiveIframe(
};
}, [containerRef]);
useLayoutEffect(() => {
setSrc(
window.location.hostname === "localhost"
? props.localSrc
: props.src,
);
setUrl(
window.location.hostname === "localhost"
? props.localSrc
: props.src,
);
}, [props.src, props.localSrc]);
return (
<div
className={
"w-full h-full flex flex-col items-stretch border border-stone-200 dark:border-stone-800 " +
"w-full h-full flex flex-col items-stretch border border-stone-100 dark:border-stone-900 " +
props.className
}
>
@@ -57,6 +71,7 @@ export function ResponsiveIframe(
<div className="flex-grow" ref={containerRef}>
<iframe
{...props}
src={src}
className="dark:bg-black"
{...dimensions}
allowFullScreen

View File

@@ -0,0 +1,17 @@
"use client";
import { usePathname } from "next/navigation";
export function BreadCrumb({
items,
}: {
items: { title: string; href: string }[];
}) {
const pathName = usePathname();
return (
<span className="text-sm font-bold">
{items.find((item) => item.href === pathName)?.title}
</span>
);
}

View File

@@ -0,0 +1,158 @@
import { ReactNode } from "react";
import { ClassRef, PropRef } from "./tags";
import { requestProject } from "./requestProject";
import { PackageIcon } from "lucide-react";
export function DocNav() {
return (
<>
<p className="mt-0 not-prose font-medium">
<DocNavLink href="#guide">Guide</DocNavLink>
</p>
<ul>
<li>
<DocNavLink href="#guide-setup">Project Setup</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Intro to CoValues
</DocNavLink>
<ul>
<li>
<DocNavLink href="#intro-to-covalues">
Declaration
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Reading
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Creation
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Editing & Subscription
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Persistence
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Remote Sync
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Public Sharing
</DocNavLink>
</li>
</ul>
</li>
<li>
<DocNavLink href="#refs-and-on-demand-subscribe">
Refs & Auto-Subscribe
</DocNavLink>
</li>
<li>
<DocNavLink href="#groups-and-permissions">
Groups & Permissions
</DocNavLink>
</li>
<li>
<DocNavLink href="#accounts-and-migrations">
Accounts & Migrations
</DocNavLink>
</li>
<li>
<DocNavLink href="#backend-workers">
Backend Workers
</DocNavLink>
</li>
</ul>
<p className="font-medium border-t -mx-4 px-4 pt-4 border-stone-200 dark:border-stone-800">
<DocNavLink href="#faq">FAQ</DocNavLink>
</p>
<NavPackage package="jazz-tools" />
<NavPackage package="jazz-react" />
<NavPackage package="jazz-browser" />
<NavPackage package="jazz-browser-media-images" />
<NavPackage package="jazz-nodejs" />
</>
);
}
export async function NavPackage({
package: packageName,
}: {
package: string;
}) {
let project = await requestProject(packageName as any);
return (
<>
<h2 className="text-sm not-prose mt-4 flex gap-1 items-center -mx-4 px-4 pt-4 border-t border-stone-200 dark:border-stone-800 ">
<code className="font-bold">{packageName}</code>{" "}
<PackageIcon size={15} strokeWidth={1.5} />
</h2>
{project.categories?.map((category) => {
return (
<details
key={category.title}
open={category.title !== "Other"}
className="[&:not([open])_summary]:after:content-['...']"
>
<summary className="block text-xs mt-2 cursor-pointer">
{category.title}
</summary>
<div className="text-sm -ml-0.5 max-w-full text-balance">
{category.children.map(
(child, i, children) =>
(i == 0 ||
child.name !==
children[i - 1]!.name) && (
<>
<a
key={child.id}
className="inline-block not-prose px-1 m-0.5 bg-stone-200 dark:bg-stone-800 rounded opacity-70 hover:opacity-100 cursor-pointer"
href={`#${packageName}/${child.name}`}
>
<code>{child.name}</code>
</a>
{"\u200B"}
</>
),
)}
</div>
</details>
);
})}
</>
);
}
export function DocNavLink({
href,
children,
}: {
href: string;
children: ReactNode;
}) {
return (
<a
href={href}
className="not-prose hover:text-black dark:hover:text-white"
>
{children}
</a>
);
}

View File

@@ -0,0 +1,396 @@
import {
CommentDisplayPart,
DeclarationReflection,
ReflectionKind,
SignatureReflection,
SomeType,
TypeContext,
} from "typedoc";
import {
ClassOrInterface,
DocComment,
FnDecl,
Highlight,
PropCategory,
PropDecl,
} from "./tags";
import { requestProject } from "./requestProject";
import { PackageIcon, Type } from "lucide-react";
export async function PackageDocs({
package: packageName,
}: {
package: string;
}) {
let project = await requestProject(packageName as any);
// console.dir(project, {depth: 10});
return (
<>
<h2 className="flex items-center gap-2">
<code>{packageName}</code> <PackageIcon />
</h2>
{project.categories?.map((category) => {
return (
<section key={category.title}>
<h3>{category.title}</h3>
{category.children.map((child) => (
<RenderPackageChild
child={child}
key={child.id}
inPackage={packageName}
/>
))}
</section>
);
})}
</>
);
}
function RenderPackageChild({
child,
inPackage,
}: {
child: DeclarationReflection;
inPackage: string;
}) {
if (
child.kind === ReflectionKind.Class ||
child.kind === ReflectionKind.Interface
) {
return (
<RenderClassOrInterface
classOrInterface={child}
inPackage={inPackage}
/>
);
} else if (child.kind === ReflectionKind.TypeAlias) {
return <RenderTypeAlias inPackage={inPackage} child={child} />;
} else if (child.kind === ReflectionKind.Function) {
return child.getAllSignatures().map((signature, i) => {
const paramTypes = printParamsWithTypes(signature);
return (
<div
key={i}
id={inPackage + "/" + child.name}
className="not-prose mt-4"
>
{
<Highlight hide={[0, 2]}>
{`function \n${printSimpleSignature(child, signature) + ":"}\n {}`}
</Highlight>
}{" "}
<span className="opacity-75 text-xs pl-1">
<Highlight>{printType(signature.type)}</Highlight>
</span>
<div className="ml-4 mt-0 text-xs opacity-75 flex">
{paramTypes.length > 0 && (
<div>
<Highlight
hide={[0, 1 + paramTypes.length]}
>{`function fn(...args: [\n${paramTypes.join(
",\n",
)}\n]) {}`}</Highlight>
</div>
)}
</div>
</div>
);
});
} else {
return (
<h4 id={inPackage + "/" + child.name}>
{child.name} {child.type?.type}
</h4>
);
}
}
function RenderTypeAlias({
inPackage,
child,
}: {
inPackage: string;
child: DeclarationReflection;
}) {
return (
<div className="mt-4">
<h4 className="not-prose" id={inPackage + "/" + child.name}>
<Highlight>{`type ${child.name}`}</Highlight>
</h4>
<p className="not-prose text-sm ml-4">
<Highlight>{`type ${child.name} = ${printType(
child.type,
)}`}</Highlight>
</p>
<div className="ml-4 mt-2 flex-[3]">
<DocComment>
{child.comment
? renderSummary(child.comment.summary)
: "⚠️ undocumented"}
</DocComment>
</div>
</div>
);
}
function RenderClassOrInterface({
inPackage,
classOrInterface: classOrInterface,
}: {
inPackage: string;
classOrInterface: DeclarationReflection;
}) {
const commentSummary = classOrInterface.comment?.summary;
return (
<ClassOrInterface
inPackage={inPackage}
name={classOrInterface.name}
doc={renderSummary(commentSummary)}
isInterface={classOrInterface.kind === ReflectionKind.Interface}
>
{classOrInterface.categories?.map((category) => (
<div key={category.title}>
<PropCategory
name={category.title}
description={renderSummary(
category.description?.filter(
(p) =>
p.kind !== "code" ||
!p.text.startsWith("```"),
),
)}
example={renderSummary(
category.description?.filter(
(p) =>
p.kind === "code" &&
p.text.startsWith("```"),
),
)}
/>
{category.children.map((prop) => (
<RenderProp
prop={prop}
klass={classOrInterface}
key={prop.id}
/>
))}
</div>
))}
</ClassOrInterface>
);
}
function renderSummary(commentSummary: CommentDisplayPart[] | undefined) {
return commentSummary?.map((part, idx) =>
part.kind === "text" ? (
<span key={idx}>{part.text}</span>
) : part.kind === "inline-tag" ? (
<code key={idx}>
{part.tag} {part.text}
</code>
) : part.text.startsWith("```") ? (
<pre key={idx} className="text-xs mt-4">
<code>
<Highlight>
{part.text.split("\n").slice(1, -1).join("\n")}
</Highlight>
</code>
</pre>
) : (
<code key={idx}>
<Highlight>{part.text.slice(1, -1)}</Highlight>
</code>
),
);
}
function RenderProp({
prop,
klass,
}: {
prop: DeclarationReflection;
klass: DeclarationReflection;
}) {
const propOrGetSig = prop.getSignature ? prop.getSignature : prop;
return prop.kind & ReflectionKind.FunctionOrMethod ? (
prop
.getAllSignatures()
.map((signature) => (
<FnDecl
key={signature.id}
signature={printSimplePropSignature(prop, klass, signature)}
paramTypes={printParamsWithTypes(signature)}
returnType={printType(signature.type)}
doc={renderSummary(signature.comment?.summary)}
example={renderSummary(
signature.comment?.getTag("@example")?.content,
)}
/>
))
) : (
<PropDecl
name={
(prop.flags.isStatic ? klass.name : "") +
(prop.name.startsWith("[") ? "" : ".") +
prop.name
}
type={printType(propOrGetSig.type)}
doc={
propOrGetSig.comment &&
renderSummary(propOrGetSig.comment.summary)
}
example={renderSummary(
propOrGetSig.comment?.getTag("@example")?.content,
)}
/>
);
}
function printSimplePropSignature(
prop: DeclarationReflection,
klass: DeclarationReflection,
signature: SignatureReflection,
): string {
return (
`${prop.flags.isStatic ? klass.name : ""}.` +
printSimpleSignature(prop, signature)
);
}
function printSimpleSignature(
item: DeclarationReflection,
signature: SignatureReflection,
) {
return `${item.name}${
signature.typeParameters?.length
? "<" +
signature.typeParameters
.map(
(tParam) =>
tParam.name +
(tParam.type
? " extends " + printType(tParam.type)
: ""),
)
.join(", ") +
">"
: ""
}(${printParams(signature)?.join(", ")})`;
}
function printParams(signature: SignatureReflection) {
return (
signature.parameters?.map((param) =>
param.name === "__namedParameters" &&
param.type?.type === "reflection"
? "{ " +
param.type.declaration.children
?.map(
(child) =>
child.name + (child.flags.isOptional ? "?" : ""),
)
.join(", ") +
" }"
: param.name + (param.defaultValue ? "?" : ""),
) || []
);
}
function printParamsWithTypes(signature: SignatureReflection) {
return (
signature.parameters?.map(
(param) =>
(param.name === "__namedParameters"
? ""
: param.name + (param.defaultValue ? "?" : "") + ": ") +
printType(param.type),
) || []
);
}
function printType(type: SomeType | undefined): string {
if (!type) return "NO TYPE";
if (type.type === "reflection") {
if (type.declaration.kind === ReflectionKind.TypeLiteral) {
if (type.declaration.signatures?.length) {
return (
type.declaration.signatures
?.map(
(sig) =>
`(${printParamsWithTypes(sig).join(
", ",
)}) => ${printType(sig.type)}`,
)
.join(" | ") || ""
);
} else {
return (
"{ " +
type.declaration.children
?.map(
(child) =>
`${child.name}: ${printType(child.type)}`,
)
.join(", ") +
" }"
);
}
}
return "TODO reflection type " + type.declaration.kind;
} else if (type.type === "reference") {
return (
type.name +
(type.typeArguments?.length
? "<" + type.typeArguments.map(printType).join(", ") + ">"
: "")
);
} else if (type.type === "intersection") {
return (
type.types
?.map((part) =>
part.needsParenthesis(TypeContext["intersectionElement"])
? `(${printType(part)})`
: printType(part),
)
.join(" & ") || "NO TYPES"
);
} else if (type.type === "union") {
return (
type.types
.sort((a, b) => (a.type === "intrinsic" ? 1 : -1))
?.map((part) =>
part.needsParenthesis(TypeContext["unionElement"])
? `(${printType(part)})`
: printType(part),
)
.join(" | ") || "NO TYPES"
);
} else if (type.type === "tuple") {
return `[${type.elements.map(printType).join(", ")}]`;
} else if (type.type === "array") {
if (type.needsParenthesis()) {
return `(${printType(type.elementType)})[]`;
} else {
return printType(type.elementType) + "[]";
}
} else if (type.type === "mapped") {
return `{[${type.parameter} in ${printType(
type.parameterType,
)}]: ${printType(type.templateType)}}`;
} else if (type.type === "indexedAccess") {
return `${printType(type.objectType)}[${printType(type.indexType)}]`;
} else if (type.type === "intrinsic") {
return type.name;
} else if (type.type === "predicate") {
return `${type.name} is ${printType(type.targetType)}`;
} else if (type.type === "query") {
return printType(type.queryType);
} else if (type.type === "literal") {
return JSON.stringify(type.value);
} else {
return "TODO type " + type.type;
}
}

View File

@@ -0,0 +1,24 @@
import { readFile } from "fs/promises";
import { ProjectReflection, Deserializer, JSONOutput } from "typedoc";
import JazzToolsDocs from "../../typedoc/jazz-tools.json";
import JazzReactDocs from "../../typedoc/jazz-react.json";
import JazzBrowserDocs from "../../typedoc/jazz-browser.json";
import JazzBrowserMediaImagesDocs from "../../typedoc/jazz-browser-media-images.json";
import JazzNodejsDocs from "../../typedoc/jazz-nodejs.json";
const docs = {
"jazz-tools": JazzToolsDocs as JSONOutput.ProjectReflection,
"jazz-react": JazzReactDocs as JSONOutput.ProjectReflection,
"jazz-browser": JazzBrowserDocs as JSONOutput.ProjectReflection,
"jazz-browser-media-images":
JazzBrowserMediaImagesDocs as JSONOutput.ProjectReflection,
"jazz-nodejs": JazzNodejsDocs as JSONOutput.ProjectReflection,
};
export async function requestProject(
packageName: keyof typeof docs,
): Promise<ProjectReflection> {
const deserializer = new Deserializer({} as any);
return deserializer.reviveProject(docs[packageName], packageName);
}

View File

@@ -0,0 +1,225 @@
import { LinkIcon } from "lucide-react";
import { ReactNode } from "react";
import { getHighlighter } from "shiki";
const highlighter = getHighlighter({
langs: ["typescript", "bash"],
theme: "css-variables", // use the theme
});
export async function Highlight({
children,
hide,
lang = "typescript",
}: {
children: string;
hide?: number[];
lang?: string;
}) {
const lines = (await highlighter).codeToThemedTokens(
children,
lang,
"css-variables",
);
return (
<code>
{lines
.filter((_, i) => !hide?.includes(i))
.map((line, i, all) => (
<>
{line.map((token, i) => (
<span key={i} style={{ color: token.color }}>
{token.content}
</span>
))}
{i !== all.length - 1 && <br />}
</>
))}
</code>
);
}
export function ClassOrInterface({
inPackage,
name,
children,
doc,
isInterface,
}: {
inPackage: string;
name: string;
children: ReactNode;
doc: ReactNode;
isInterface?: boolean;
}) {
return (
<div className="relative not-prose">
<a
id={inPackage + "/" + name}
href={"#" + inPackage + "/" + name}
className="absolute md:top-[3.5rem] -ml-6 w-4 flex items-center justify-center opacity-0 peer-group-hover:opacity-100 target:opacity-100"
tabIndex={-1}
>
<LinkIcon size={15} />
</a>
<h4 className="peer sticky top-0 pt-[0.5rem] md:top-[2.5rem] md:pt-[3rem] bg-stone-50 dark:bg-stone-950 z-20">
<a href={"#" + inPackage + "/" + name}>
<Highlight>
{(isInterface ? "interface " : "class ") + name}
</Highlight>
</a>
</h4>
<div className="pl-2">
<div className=" mt-4 text-sm">{doc}</div>
<div className="">{children}</div>
</div>
</div>
);
}
export function ClassRef({ name }: { name: string }) {
return <Highlight hide={[0]}>{`class\n${name}`}</Highlight>;
}
export function PropRef({ on, prop }: { on?: string; prop: string }) {
return on ? (
<Highlight>{`${on}.${prop}`}</Highlight>
) : (
<Highlight>{prop}</Highlight>
);
}
export function PropDecl({
name,
type,
doc,
example,
}: {
name?: string;
type?: string;
doc: ReactNode;
example?: ReactNode;
}) {
return (
<div className="py-2 border-t border-stone-200 dark:border-stone-800 mt-4 text-sm">
<div>
{name && <Highlight>{name + ":"}</Highlight>}
{" "}
{type && (
<span className="opacity-75 text-xs pl-1">
<Highlight
hide={[0, 1, 2 + type.split("\n").length]}
>{`class X {\nprop:\n${type}`}</Highlight>
</span>
)}
</div>
<div className="lg:flex gap-2">
<div className="ml-4 mt-2 flex-[3]">
<DocComment>{doc || "⚠️ undocumented"}</DocComment>
</div>
{example && (
<div className="ml-4 lg:ml-0 lg:mt-0 flex-[1] relative w-full overflow-x-scroll col-span-2 pl-4 md:pl-0 md:mt-0 text-xs opacity-60 group-hover:opacity-100">
<div className="opacity-30 text-xs -mb-4">Example:</div>
{example}
</div>
)}
</div>
</div>
);
}
export function FnDecl({
signature,
paramTypes,
returnType,
doc,
example,
}: {
signature: string;
paramTypes: string[];
returnType: string;
doc: ReactNode;
example: ReactNode;
}) {
return (
<div className="py-2 border-t border-stone-200 dark:border-stone-800 mt-4 text-sm">
<div>
{<Highlight>{signature + ":"}</Highlight>}{" "}
<span className="opacity-75 text-xs pl-1">
<Highlight>{returnType}</Highlight>
</span>
</div>
<div className="ml-4 mt-2 text-xs opacity-75 flex">
{paramTypes.length > 0 && (
<div>
<Highlight
hide={[0, 1 + paramTypes.length]}
>{`function fn(...args: [\n${paramTypes.join(
",\n",
)}\n]) {}`}</Highlight>
</div>
)}
</div>
<div className="lg:flex gap-2">
<div className="ml-4 mt-2 flex-[3]">
<DocComment>{doc || "⚠️ undocumented"}</DocComment>
</div>
{example && (
<div className="flex-[1] relative w-full overflow-x-scroll col-span-2 pl-4 md:pl-0 mt-6 md:mt-0 text-xs opacity-60 group-hover:opacity-100">
<div className="opacity-30 text-xs -mb-4">Example:</div>
{example}
</div>
)}
</div>
</div>
);
}
export function PropCategory({
name,
description,
example,
}: {
name: string;
description?: ReactNode;
example?: ReactNode;
}) {
return (
<>
<div className="col-span-6 mt-8 -mb-4 text-[0.7em] uppercase font-medium tracking-widest opacity-50">
{name}
</div>
{description && <PropDecl doc={description} example={example} />}
</>
);
}
export function DocComment({ children }: { children: ReactNode }) {
return (
<div className="prose-inner-sm max-w-xl leading-snug">{children}</div>
);
}
export function NewCoValueExplainer({ type }: { type: string }) {
return (
<>
Creates a new <ClassRef name={type} /> with the given initial
values. The <ClassRef name={type} /> is immediately persisted
locally and synced to connected peers. Access rights are determined
by roles in the owner <ClassRef name="Group" /> or directly by the
owner <ClassRef name="Account" />.
</>
);
}
export function RefValueExplainer({ propName }: { propName: string }) {
return (
<>
Note that even non-optional <PropRef on="co" prop="ref(...)" />{" "}
{propName} might be <Highlight>null</Highlight> if the referenced
value isn&apos;t loaded yet. Accessing one will cause it to be
loaded if done from inside a <i>Subscription Context</i>.
</>
);
}

View File

@@ -2,8 +2,10 @@ export function Slogan(props: { children: ReactNode; small?: boolean }) {
return (
<div
className={[
"leading-snug mb-5 max-w-3xl text-stone-700 dark:text-stone-200",
props.small ? "text-lg lg:text-xl -mt-2" : "text-xl lg:text-2xl -mt-5",
"leading-snug tracking-tight mb-5 max-w-4xl text-stone-700 dark:text-stone-500",
props.small
? "text-lg lg:text-xl -mt-2"
: "text-3xl lg:text-4xl -mt-5",
].join(" ")}
>
{props.children}
@@ -21,9 +23,9 @@ export function Grid({
return (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",
"mt-10 items-stretch",
className
className,
)}
>
{children}
@@ -43,14 +45,16 @@ export function GridFeature(props: {
return (
<div
className={[
"p-4 flex items-center gap-2",
"p-4 flex flex-col items-center justify-center gap-2",
"not-prose text-base",
"border border-stone-200 dark:border-stone-800 rounded-xl shadow-sm",
"border border-stone-200 dark:border-stone-800 rounded-xl",
props.className || "",
].join(" ")}
>
<div className="text-stone-500 mr-2">{props.icon}</div>
{props.children}
<div className="text-stone-700 dark:text-stone-300">
{props.children}
</div>
</div>
);
}
@@ -59,7 +63,7 @@ export function GridCard(props: { children: ReactNode; className?: string }) {
return (
<div
className={[
"p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
"col-span-2 p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
"border border-stone-200 dark:border-stone-800 rounded-xl shadow-sm",
props.className,
].join(" ")}
@@ -69,26 +73,34 @@ export function GridCard(props: { children: ReactNode; className?: string }) {
);
}
export function MultiplayerIcon() {
export function MultiplayerIcon({
color,
strokeWidth,
size,
}: {
color?: string;
strokeWidth?: number;
size: number;
}) {
return (
<div className="w-8 h-8 -my-1 -mr-2 relative z-0">
<div className="relative z-0" style={{ width: size, height: size }}>
<MousePointer2Icon
size="20"
absoluteStrokeWidth
strokeWidth={2}
className="absolute top-1 right-0"
size={0.6 * size}
strokeWidth={(strokeWidth || 1) / 0.6}
color={color}
className="absolute top-0 right-0"
/>
<MousePointer2Icon
size="16"
absoluteStrokeWidth
strokeWidth={2}
className="absolute bottom-1 left-0 -scale-x-100"
size={0.5 * size}
strokeWidth={(strokeWidth || 1) / 0.5}
color={color}
className="absolute bottom-0 left-0 -scale-x-100"
/>
</div>
);
}
export function ComingSoonBadge({when = "soon"}: {when?: string}) {
export function ComingSoonBadge({ when = "soon" }: { when?: string }) {
return (
<span className="bg-stone-100 dark:bg-stone-900 text-stone-500 dark:text-stone-400 border border-stone-300 dark:border-stone-700 text-[0.6rem] px-1 py-0.5 rounded-xl align-text-top">
Coming&nbsp;{when}
@@ -102,7 +114,7 @@ import { HandIcon, MousePointer2Icon, TextCursorIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export function ResponsiveIframe(
props: IframeHTMLAttributes<HTMLIFrameElement>
props: IframeHTMLAttributes<HTMLIFrameElement> & { localSrc: string },
) {
return <ResponsiveIframeClient {...props} />;
}

View File

@@ -20,7 +20,13 @@ export function JazzLogo({ className }: { className?: string }) {
);
}
export function GcmpLogo({ monochrome, className }: { monochrome?: boolean, className?: string }) {
export function GcmpLogo({
monochrome,
className,
}: {
monochrome?: boolean;
className?: string;
}) {
return (
<svg
viewBox="0 0 557 164"
@@ -72,7 +78,7 @@ export function GcmpLogo({ monochrome, className }: { monochrome?: boolean, clas
</g>
<defs>
<clipPath id="clip0_12197_1172">
<rect width="557" height="164" fill="white"/>
<rect width="557" height="164" fill="white" />
</clipPath>
</defs>
</svg>

Some files were not shown because too many files have changed in this diff Show More