Compare commits

...

362 Commits

Author SHA1 Message Date
Anselm
7213b1bfa3 Release 2024-07-13 13:32:50 +01:00
Anselm Eickhoff
11f0770f08 Merge pull request #230 from tobiaslins/add-support-for-in
Add support for property existence
2024-07-13 13:30:15 +01:00
Tobias Lins
44e6dc3ae8 Remove useless check 2024-07-13 13:57:37 +02:00
Tobias Lins
b5d20d2488 Better implementation 2024-07-13 13:26:28 +02:00
Tobias Lins
0185545838 Add support for property existence 2024-07-13 12:26:10 +02:00
Anselm
b102964743 Get rid of husky for now 2024-07-12 11:56:54 +01:00
Anselm
216d50a09c Remove old homepage build 2024-07-12 11:55:44 +01:00
Anselm
07ea59fdcb Release 2024-07-12 11:14:15 +01:00
Anselm
932a84a47f Update to Effect 3.5.2 2024-07-12 11:13:41 +01:00
Anselm
34dda7bdbd Release 2024-07-05 11:18:53 +01:00
Anselm
49fa153581 Merge branch 'fix-fs' 2024-07-05 11:17:54 +01:00
Anselm
c80b827775 Release 2024-06-30 16:28:53 +01:00
Anselm Eickhoff
a2bf9f988a Merge pull request #218 from Schniz/fix-mutating-nullable-field 2024-06-30 08:31:57 +01:00
Gal Schlezinger
ac27b2d5c2 jazz-tools: allow to mutate nullable fields into null
when having a co.encoded(Schema.NullOr(Schema.String)), construction
with null works well, but mutating a value into null throws.
This commit fixes it and adds a test that verifies it actually works.
2024-06-30 09:49:15 +03:00
Anselm
c813518fdc Release 2024-06-28 16:27:15 +01:00
Anselm
d5034ed5c3 Provide current res in ProgressiveImg 2024-06-28 16:26:03 +01:00
Anselm
cf2c29a365 Use verce lanalytics & speed insights 2024-06-28 14:54:58 +01:00
Anselm
d948823db6 Add package manager to package.json 2024-06-28 12:08:23 +01:00
Anselm
060ad4630d Resurrect inspector 2024-06-27 16:54:23 +01:00
Anselm
0ddceac4c0 Fix FS bugs 2024-06-26 13:45:09 +01:00
Anselm
a862cb8819 Release 2024-06-25 15:13:55 +01:00
Anselm Eickhoff
4246aed7db Merge pull request #214 from gardencmp/effect-streams-for-peers
Effect streams for peers
2024-06-25 15:12:56 +01:00
Anselm
41554e0e0b Release 2024-06-25 13:57:24 +01:00
Anselm
93c4d8155e Fix CoList.toJSON() 2024-06-25 13:56:20 +01:00
Anselm
24eefd49f1 Release 2024-06-25 13:46:54 +01:00
Anselm
e712f1e8ef Fix circular toJSON bug #215 2024-06-25 13:46:14 +01:00
Anselm
33db0fd654 Keep old hompage lockfile (unrelated) 2024-06-13 22:17:00 +01:00
Anselm
478ded93de Merge branch 'main' into effect-streams-for-peers 2024-06-13 22:12:33 +01:00
Anselm
89ad1fb79d Fix remaining tests by introducing even more special cases 2024-06-13 22:11:25 +01:00
Anselm Eickhoff
1ba40806ec Merge pull request #213 from tobiaslins/fix-demo-auth-nextjs
Fix DemoAuth for Next.js
2024-06-13 17:43:11 +01:00
Tobias Lins
73ae281e4a Fix demoAuth for next.js 2024-06-13 18:41:29 +02:00
Anselm
a35353c987 Use Effect streams 2024-06-13 15:58:21 +01:00
Anselm
1cb91003cc Release 2024-06-12 15:12:00 +01:00
Anselm
d850022491 Fix #210 2024-06-12 15:10:22 +01:00
Anselm
93792ab6f6 Remove lofi conf badge 2024-06-06 12:02:31 +01:00
Anselm
95dfe7af6a Guide fix 2024-06-04 17:56:34 +01:00
Anselm
734258eb17 Release 2024-06-04 12:14:59 +01:00
Anselm
f3bcf96fad Also cache agent ID in RawControlledAccount 2024-06-04 12:13:54 +01:00
Anselm
5cf0bc1911 Release 2024-06-04 12:09:35 +01:00
Anselm
d32a6b275f Formatting 2024-06-04 12:09:16 +01:00
Anselm
6caba9f8e7 Cache currentAgentID in RawAccount 2024-06-04 12:06:13 +01:00
Anselm
641f1dbfbe Release 2024-06-03 17:33:59 +01:00
Anselm
58d9a104d6 Fix CoMaps not initialising properly when passing too many init options 2024-06-03 17:33:24 +01:00
Anselm
7b9d24c8ef Release 2024-06-01 17:25:06 +02:00
Anselm Eickhoff
4225fdd537 Merge pull request #200 from gdorsi/main
fix: improve compatibility with React compiler and concurrent features
2024-06-01 17:24:17 +02:00
Anselm
9fdc91c6de Add changeset 2024-06-01 17:23:18 +02:00
Anselm
93d8c85e5c Formatting 2024-06-01 17:22:38 +02:00
Anselm
929cddc3c3 Release 2024-06-01 17:13:02 +02:00
Anselm
e0bc63f016 Provide way to create accounts as another account 2024-06-01 17:12:42 +02:00
Anselm
b29ac306ea Release 2024-06-01 14:00:50 +02:00
Anselm
e8e883f4d6 Ability to add seed accounts to DemoAuth 2024-06-01 14:00:26 +02:00
Guido D'Orsi
3325ff1cd6 fix: make Jazz react compiler ready 2024-05-31 10:19:33 +02:00
Anselm
4fe14f03b4 Formatting fixes 2024-05-31 09:13:25 +02:00
Anselm
90e2a661e4 Very last fix 2024-05-31 09:00:56 +02:00
Anselm
6ed53ecb79 Last fixes 2024-05-31 08:59:41 +02:00
Anselm
c18775766c More fixes 2024-05-31 08:56:11 +02:00
Anselm
4bb3a6209a Doc fixes 2024-05-31 08:44:45 +02:00
Anselm
0f44a547a4 Lots more guide progress 2024-05-28 17:10:59 +01:00
Anselm
1e2f6d8f14 Release 2024-05-26 20:44:05 +01:00
Anselm Eickhoff
7e5b176930 Merge pull request #199 from tobiaslins/implement-loading-ui
Implement basic `loading` component that is rendered when migrations …
2024-05-26 20:40:00 +01:00
Anselm
b420eab503 Fix type for Provider 2024-05-26 20:37:55 +01:00
Tobias Lins
b370e2e14e Merge branch 'main' into implement-loading-ui 2024-05-26 20:48:38 +02:00
Tobias Lins
1fabee297d Implement basic loading component that is rendered when migrations are running or jazz not ready yet 2024-05-26 20:46:52 +02:00
Anselm
484dc460c5 Lots of doc progress 2024-05-26 17:39:16 +01:00
Anselm
0cb8756124 Fix lint error 2024-05-26 16:16:25 +01:00
Anselm
95d0f0221b Release 2024-05-26 16:15:18 +01:00
Anselm
0c9c0fcd60 Clean up loading & subscription API 2024-05-26 16:14:01 +01:00
Anselm
8be0dd133c Doc improvements 2024-05-26 16:13:25 +01:00
Anselm
e68e0ada0d Chage hero grid 2024-05-25 21:27:20 +01:00
Anselm
49a7349e4d Update copyright year 2024-05-25 21:27:11 +01:00
Anselm
979c7241a4 Guide progress, licenses 2024-05-25 20:54:53 +01:00
Anselm
e011e4a049 Add local-first conf badge 2024-05-25 19:46:36 +01:00
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
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
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
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
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
485 changed files with 43990 additions and 62951 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-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", "counter-js-auth0"]
example: ["chat", "pets", "todo", "inspector"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
cache-dependency-path: yarn.lock
cache: 'pnpm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -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,13 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-homepage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- 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: 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", "counter-js-auth0"]
example: ["chat", "pets", "todo", "inspector"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
- uses: actions/checkout@v3
@@ -116,37 +88,4 @@ jobs:
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: ./examples/${{ matrix.example }}
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 }}
- 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}};
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
working-directory: ./examples/${{ matrix.example }}

4
.gitignore vendored
View File

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

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

View File

@@ -1,4 +1,4 @@
Copyright 2023, Garden Computing, Inc.
Copyright 2024, Garden Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

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

@@ -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

@@ -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,599 @@
# jazz-example-chat
## 0.0.66
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-react@0.7.19
## 0.0.65
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- jazz-react@0.7.18
- jazz-tools@0.7.18
## 0.0.64
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- jazz-react@0.7.17
- jazz-tools@0.7.17
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-react@0.7.16
## 0.0.62
### Patch Changes
- Updated dependencies
- jazz-react@0.7.15
## 0.0.61
### Patch Changes
- Updated dependencies
- cojson@0.7.14
- jazz-tools@0.7.14
- jazz-react@0.7.14
## 0.0.60
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.13
- jazz-react@0.7.13
## 0.0.59
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.12
- jazz-react@0.7.12
## 0.0.58
### Patch Changes
- Updated dependencies
- cojson@0.7.11
- jazz-react@0.7.11
- jazz-tools@0.7.11
## 0.0.57
### Patch Changes
- Updated dependencies
- cojson@0.7.10
- jazz-react@0.7.10
- jazz-tools@0.7.10
## 0.0.56
### Patch Changes
- Updated dependencies
- cojson@0.7.9
- jazz-react@0.7.9
- jazz-tools@0.7.9
## 0.0.55
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.8
- jazz-react@0.7.8
## 0.0.54
### Patch Changes
- Updated dependencies [9fdc91c]
- jazz-react@0.7.7
## 0.0.53
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.6
- jazz-react@0.7.6
## 0.0.52
### Patch Changes
- Updated dependencies
- jazz-react@0.7.5
## 0.0.51
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.50
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-react@0.7.3
## 0.0.49
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.48
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-react@0.7.1
## 0.0.47
### Patch Changes
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [96c494f]
- Updated dependencies [59c18c3]
- Updated dependencies [19f52b7]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [d8fe2b1]
- Updated dependencies [19004b4]
- Updated dependencies [a78f168]
- Updated dependencies [1200aae]
- Updated dependencies [60d5ca2]
- Updated dependencies [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [1a35307]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [bf0f8ec]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- Updated dependencies [8636319]
- Updated dependencies [01ac646]
- Updated dependencies [a5e68a4]
- Updated dependencies [8636319]
- Updated dependencies [952982e]
- Updated dependencies [1a35307]
- Updated dependencies [5fa277c]
- Updated dependencies [60d5ca2]
- Updated dependencies [21771c4]
- Updated dependencies [77c2b56]
- Updated dependencies [63374cc]
- Updated dependencies [d2e03ff]
- Updated dependencies [354bdcd]
- Updated dependencies [ece35b3]
- Updated dependencies [60d5ca2]
- Updated dependencies [69ac514]
- Updated dependencies [f8a5c46]
- Updated dependencies [f0f6f1b]
- Updated dependencies [e5eed5b]
- Updated dependencies [1a44f87]
- Updated dependencies [627d895]
- Updated dependencies [1200aae]
- Updated dependencies [63374cc]
- Updated dependencies [ece35b3]
- Updated dependencies [38d4410]
- Updated dependencies [85d2b62]
- Updated dependencies [fd86c11]
- Updated dependencies [52675c9]
- jazz-tools@0.7.0
- cojson@0.7.0
- jazz-react@0.7.0
- hash-slash@0.2.0
## 0.0.47-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- cojson@0.7.0-alpha.42
- jazz-react@0.7.0-alpha.42
## 0.0.47-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-react@0.7.0-alpha.41
## 0.0.47-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.47-alpha.39
### Patch Changes
- Updated dependencies
- cojson@0.7.0-alpha.39
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
## 0.0.47-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
- cojson@0.7.0-alpha.38
## 0.0.47-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- cojson@0.7.0-alpha.37
- jazz-tools@0.7.0-alpha.37
## 0.0.47-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- cojson@0.7.0-alpha.36
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
## 0.0.47-alpha.35
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson@0.7.0-alpha.35
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
## 0.0.47-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-react@0.7.0-alpha.34
## 0.0.47-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.47-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- hash-slash@0.2.0-alpha.3
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
## 0.0.47-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.47-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.47-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- cojson@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.47-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- cojson@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.47-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- cojson@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.47-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.47-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.47-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- cojson@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.47-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.47-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.47-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
## 0.0.47-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
## 0.0.47-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.47-alpha.18
### Patch Changes
- jazz-react@0.7.0-alpha.18
## 0.0.47-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.47-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.47-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.47-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.47-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.47-alpha.12
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.47-alpha.11
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
- cojson@0.7.0-alpha.11
## 0.0.47-alpha.10
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
- cojson@0.7.0-alpha.10
## 0.0.47-alpha.9
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.47-alpha.8
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.47-alpha.7
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
- cojson@0.7.0-alpha.7
## 0.0.47-alpha.6
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.47-alpha.5
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
- cojson@0.7.0-alpha.5
## 0.0.47-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.47-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.47-alpha.2
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.2
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.47-alpha.1
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.1
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
- cojson@0.7.0-alpha.1
## 0.0.47-alpha.0
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.0
- jazz-react@0.7.0-alpha.0
- jazz-tools@0.7.0-alpha.0
- cojson@0.7.0-alpha.0
## 0.0.46
### Patch Changes

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.66",
"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,43 +0,0 @@
import { useAutoSub } from 'jazz-react';
import { Chat, Message } from './dataModel.ts';
export function ChatWindow(props: { chatId: Chat['id'] }) {
const chat = useAutoSub(props.chatId);
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
{
chat.map((msg, i) => (
<ChatBubble key={msg?.id}
text={msg?.text}
by={chat.meta.edits[i].by?.profile?.name}
byMe={chat.meta.edits[i].by?.isMe}
at={chat.meta.edits[i].at} />
))
}
<ChatInput onSubmit={(text) => {
const msg = chat.meta.group.createMap<Message>({ text });
chat.append(msg.id);
}}/>
</div> : <div>Loading...</div>;
}
function ChatBubble(props: { text?: string, by?: string, at?: Date, byMe?: boolean }) {
return <div className={`${props.byMe ? 'items-end' : 'items-start'} 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.text }
</div>
<div className='text-xs text-neutral-500 ml-2'>
{ props.by } { props.at?.getHours() }:{ props.at?.getMinutes() }
</div>
</div>;
}
function ChatInput(props: { onSubmit: (text: string) => void }) {
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white 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,4 +0,0 @@
import { CoMap, CoList } from 'cojson';
export type Chat = CoList<Message['id']>;
export type Message = CoMap<{ text: string }>;

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

@@ -0,0 +1,34 @@
# jazz-example-chat
## 0.0.49
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- cojson-transport-ws@0.7.18
## 0.0.48
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- cojson-transport-ws@0.7.17
## 0.0.47
### Patch Changes
- Updated dependencies
- cojson@0.6.7
- jazz-react@0.5.5
- jazz-react-auth-local@0.4.18
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -0,0 +1,42 @@
# Jazz Chat Example
Live version: https://example-chat.jazz.tools
## Installing & running the example locally
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/chat
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/chat # or any other directory
tar -xf /tmp/jazz-example-chat-* --strip-components 1 -C ~/jazz-examples/chat
cd ~/jazz-examples/chat
```
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
pnpm install
```
Start the dev server:
```bash
pnpm dev
```
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx 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

@@ -0,0 +1,56 @@
job "inspector$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "inspector$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,49 @@
{
"name": "jazz-inspector",
"private": true,
"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",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"hash-slash": "workspace:*",
"cojson": "workspace:*",
"cojson-transport-ws": "workspace:*",
"effect": "^3.5.2",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^5.0.10"
}
}

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,309 @@
import ReactDOM from "react-dom/client";
import {
RawAccount,
CoID,
RawCoValue,
SessionID,
LocalNode,
AgentSecret,
AccountID,
cojsonInternals,
WasmCrypto,
} from "cojson";
import { clsx } from "clsx";
import { AccountInfo, CoJsonTree, Tag } from "./cojson-tree";
import { useEffect, useState } from "react";
import { createWebSocketPeer } from "cojson-transport-ws";
import { Effect } from "effect";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
function App() {
const [accountID, setAccountID] = useState<CoID<RawAccount>>(
localStorage["inspectorAccountID"]
);
const [accountSecret, setAccountSecret] = useState<AgentSecret>(
localStorage["inspectorAccountSecret"]
);
const [coValueId, setCoValueId] = useState<CoID<RawCoValue>>(
window.location.hash.slice(2) as CoID<RawCoValue>
);
useEffect(() => {
window.addEventListener("hashchange", () => {
setCoValueId(window.location.hash.slice(2) as CoID<RawCoValue>);
});
});
const [localNode, setLocalNode] = useState<LocalNode>();
useEffect(() => {
if (!accountID || !accountSecret) return;
WasmCrypto.create().then(async (crypto) => {
const wsPeer = await Effect.runPromise(
createWebSocketPeer({
id: "mesh",
websocket: new WebSocket("wss://mesh.jazz.tools"),
role: "server",
})
);
const node = await LocalNode.withLoadedAccount({
accountID: accountID,
accountSecret: accountSecret,
sessionID: cojsonInternals.newRandomSessionID(accountID),
peersToLoadFrom: [wsPeer],
crypto,
migration: async () => {
console.log("Not running any migration in inspector");
},
});
setLocalNode(node);
});
}, [accountID, accountSecret]);
return (
<div className="flex flex-col items-center w-screen h-screen p-2 gap-2">
<div className="flex gap-2 items-center">
Account
<input
className="border p-2 rounded"
placeholder="Account ID"
value={accountID}
onChange={(e) => {
setAccountID(e.target.value as AccountID);
localStorage["inspectorAccountID"] = e.target.value;
}}
/>
<input
type="password"
className="border p-2 rounded"
placeholder="Account Secret"
value={accountSecret}
onChange={(e) => {
setAccountSecret(e.target.value as AgentSecret);
localStorage["inspectorAccountSecret"] = e.target.value;
}}
/>
{localNode ? (
<AccountInfo accountID={accountID} node={localNode} />
) : (
""
)}
</div>
<div className="flex gap-2 items-center">
CoValue ID
<input
className="border p-2 rounded min-w-[20rem]"
placeholder="CoValue ID"
value={coValueId}
onChange={(e) =>
setCoValueId(e.target.value as CoID<RawCoValue>)
}
/>
</div>
{coValueId && localNode ? (
<Inspect coValueId={coValueId} node={localNode} />
) : null}
</div>
);
}
// function ImageCoValue({ value }: { value: ImageDefinition["_shape"] }) {
// const keys = Object.keys(value);
// const keyIncludingRes = keys.find((key) => key.includes("x"));
// const idToResolve = keyIncludingRes
// ? value[keyIncludingRes as `${number}x${number}`]
// : null;
// if (!idToResolve) return <div>Can't find image</div>;
// const [blobURL, setBlobURL] = useState<string>();
// useEffect(() => {
// })
// return (
// <img
// src={image?.blobURL || value.placeholderDataURL}
// alt="placeholder"
// />
// );
// }
function Inspect({
coValueId,
node,
}: {
coValueId: CoID<RawCoValue>;
node: LocalNode;
}) {
const [coValue, setCoValue] = useState<RawCoValue | "unavailable">();
useEffect(() => {
return node.subscribe(coValueId, (coValue) => {
setCoValue(coValue);
});
}, [node, coValueId]);
if (coValue === "unavailable") {
return <div>Unavailable</div>;
}
const values = coValue?.toJSON() || {};
const isImage =
typeof values === "object" && "placeholderDataURL" in values;
const isGroup = coValue?.core.header.ruleset?.type === "group";
const entires = Object.entries(values as any) as [string, string][];
const onlyCoValues = entires.filter(([key]) => key.startsWith("co_"));
let title = "";
if (isImage) {
title = "Image";
} else if (isGroup) {
title = "Group";
}
return (
<div className="mb-auto">
<h1 className="text-xl font-bold mb-2">
Inspecting {title}{" "}
<span className="text-gray-500 text-sm">{coValueId}</span>
</h1>
{isGroup ? (
<p>
{onlyCoValues.length > 0 ? <h3>Permissions</h3> : ""}
<div className="flex gap-2 flex-col">
{onlyCoValues?.map(([key, value]) => (
<div className="flex gap-1 items-center">
<span className="bg-gray-200 text-xs px-2 py-0.5 rounded">
{value}
</span>
<AccountInfo
accountID={key as CoID<RawAccount>}
node={node}
/>
</div>
))}
</div>
</p>
) : (
<span className="">
Group{" "}
<Tag href={`#/${coValue?.group.id}`}>
{coValue?.group.id}
</Tag>
</span>
)}
{/* {isImage ? (
<div className="my-2">
<ImageCoValue value={values as any} />
</div>
) : null} */}
<pre className="max-w-[80vw] overflow-scroll text-sm mt-4">
<CoJsonTree coValueId={coValueId} node={node} />
</pre>
<h2 className="text-lg font-semibold mt-10 mb-4">Sessions</h2>
{coValue && <Sessions coValue={coValue} node={node} />}
</div>
);
}
function Sessions({ coValue, node }: { coValue: RawCoValue; node: LocalNode }) {
const validTx = coValue.core.getValidSortedTransactions();
return (
<div className="max-w-[80vw] border rounded">
{[...coValue.core.sessionLogs.entries()].map(
([sessionID, session]) => (
<div
key={sessionID}
className="mv-10 flex gap-2 border-b p-5 flex-wrap flex-col"
>
<div className="flex gap-2 flex-row">
<SessionInfo
sessionID={sessionID}
transactionCount={session.transactions.length}
node={node}
/>
</div>
<div className="flex gap-1 flex-wrap max-h-64 overflow-y-auto p-1 bg-gray-50 rounded">
{session.transactions.map((tx, txIdx) => {
const correspondingValidTx = validTx.find(
(validTx) =>
validTx.txID.sessionID === sessionID &&
validTx.txID.txIndex == txIdx
);
return (
<div
key={txIdx}
className={clsx(
"text-xs flex-1 p-2 border rounded min-w-36 max-w-40 overflow-scroll bg-white",
!correspondingValidTx &&
"bg-red-50 border-red-100"
)}
>
<div>
{new Date(
tx.madeAt
).toLocaleString()}
</div>
<div>{tx.privacy}</div>
<pre>
{correspondingValidTx
? JSON.stringify(
correspondingValidTx.changes,
undefined,
2
)
: "invalid/undecryptable"}
</pre>
</div>
);
})}
</div>
<div className="text-xs">
{session.lastHash} / {session.lastSignature}{" "}
</div>
</div>
)
)}
</div>
);
}
function SessionInfo({
sessionID,
transactionCount,
node,
}: {
sessionID: SessionID;
transactionCount: number;
node: LocalNode;
}) {
let Prefix = sessionID.startsWith("co_") ? (
<AccountInfo
accountID={sessionID.split("_session_")[0] as CoID<RawAccount>}
node={node}
/>
) : (
<pre className="text-xs">{sessionID.split("_session_")[0]}</pre>
);
return (
<div>
{Prefix}
<div>
<span className="text-xs">
Session {sessionID.split("_session_")[1]}
</span>
<span className="text-xs text-gray-600 font-medium">
{" "}
- {transactionCount} txs
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,249 @@
import clsx from "clsx";
import { AccountID, CoID, LocalNode, RawAccount, RawCoMap, RawCoValue } from "cojson";
import { useEffect, useState } from "react";
import { LinkIcon } from "./link-icon";
export function CoJsonTree({
coValueId,
node,
}: {
coValueId: CoID<RawCoValue>;
node: LocalNode;
}) {
const [coValue, setCoValue] = useState<RawCoValue | "unavailable">();
useEffect(() => {
return node.subscribe(coValueId, (value) => {
setCoValue(value);
});
});
if (coValue === "unavailable") {
return <div className="text-red-500">Unavailable</div>;
}
const values = coValue?.toJSON() || {};
return <RenderCoValueJSON json={values} node={node} />;
}
function RenderObject({
json,
node,
}: {
json: Record<string, any>;
node: LocalNode;
}) {
const [limit, setLimit] = useState(10);
const hasMore = Object.keys(json).length > limit;
const entries = Object.entries(json).slice(0, limit);
return (
<div className="flex gap-x-1 flex-col font-mono text-xs overflow-auto">
{"{"}
{entries.map(([key, value]) => {
return (
<RenderObjectValue
property={key}
value={value}
node={node}
/>
);
})}
{hasMore ? (
<div
className="text-gray-500 cursor-pointer"
onClick={() => setLimit((l) => l + 10)}
>
... {Object.keys(json).length - limit} more
</div>
) : null}
{"}"}
</div>
);
}
function RenderObjectValue({
property,
value,
node,
}: {
property: string;
value: any;
node: LocalNode;
}) {
const [shouldLoad, setShouldLoad] = useState(false);
const isCoValue =
typeof value === "string" ? value?.startsWith("co_") : false;
return (
<div className={clsx(`flex group`)}>
<div className="text-gray-500 flex items-start">
<div className="flex items-center">
<RenderCoValueJSON json={property} node={node} />:{" "}
</div>
</div>
{isCoValue ? (
<div className={clsx(shouldLoad && "pb-2")}>
<div className="flex items-center ">
<div onClick={() => setShouldLoad((s) => !s)}>
<div className="w-8 text-center text-gray-700 font-mono px-1 text-xs rounded hover:bg-gray-300 cursor-pointer">
{shouldLoad ? `-` : `...`}
</div>
</div>
<a
href={`#/${value}`}
className="ml-2 group-hover:block hidden"
>
<LinkIcon />
</a>
</div>
<span>
{shouldLoad ? (
<CoJsonTree coValueId={value} node={node} />
) : null}
</span>
</div>
) : (
<div className="">
<RenderCoValueJSON json={value} node={node} />
</div>
)}
</div>
);
}
function RenderCoValueArray({ json, node }: { json: any[]; node: LocalNode }) {
const [limit, setLimit] = useState(10);
const hasMore = json.length > limit;
const entries = json.slice(0, limit);
return (
<div className="flex gap-x-1 flex-col font-mono text-xs overflow-auto">
{entries.map((value, idx) => {
return (
<div key={idx} className="flex gap-x-1">
<RenderCoValueJSON json={value} node={node} />
</div>
);
})}
{hasMore ? (
<div
className="text-gray-500 cursor-pointer"
onClick={() => setLimit((l) => l + 10)}
>
... {json.length - limit} more
</div>
) : null}
</div>
);
}
function RenderCoValueJSON({
json,
node,
}: {
json:
| Record<string, any>
| any[]
| string
| null
| number
| boolean
| undefined;
node: LocalNode;
}) {
if (typeof json === "undefined") {
return <>"undefined"</>;
} else if (Array.isArray(json)) {
return (
<div className="">
<span className="text-gray-500">[</span>
<div className="ml-2">
<RenderCoValueArray json={json} node={node} />
</div>
<span className="text-gray-500">]</span>
</div>
);
} else if (
typeof json === "object" &&
json &&
Object.getPrototypeOf(json) === Object.prototype
) {
return <RenderObject json={json} node={node} />;
} else if (typeof json === "string") {
if (json?.startsWith("co_")) {
if (json.includes("_session_")) {
return (
<>
<AccountInfo accountID={json.split("_session_")[0] as AccountID} node={node}/>{" "}
(sess {json.split("_session_")[1]})
</>
);
} else {
return (
<>
<a className="underline" href={`#/${json}`}>
{'"'}
{json}
{'"'}
</a>
</>
);
}
} else {
return <div className="truncate max-w-64 ml-1">{json}</div>;
}
} else {
return <div className="truncate max-w-64">{JSON.stringify(json)}</div>;
}
}
export function AccountInfo({ accountID, node }: { accountID: CoID<RawAccount>, node: LocalNode }) {
const [name, setName] = useState<string | null>(null);
useEffect(() => {
(async () => {
const account = await node.load(accountID);
if (account === "unavailable") return;
const profileID = account?.get("profile");
if (profileID === undefined) return;
const profile = await node.load(profileID as CoID<RawCoMap>);
if (profile === "unavailable") return;
setName(profile?.get("name") as string);
})()
}, [accountID, node]);
return name ? (
<Tag href={`#/${accountID}`} title={accountID}><h1>{name}</h1></Tag>
) : (
<Tag href={`#/${accountID}`}>{accountID}</Tag>
);
}
export function Tag({
children,
href,
title
}: {
children: React.ReactNode;
href?: string;
title?: string;
}) {
if (href) {
return (
<a
href={href}
className="border text-xs px-2 py-0.5 rounded hover:underline"
title={title}
>
{children}
</a>
);
}
return (
<span className="border text-xs px-2 py-0.5 rounded">{children}</span>
);
}

View File

@@ -0,0 +1,18 @@
export function LinkIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-3 h-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
/>
</svg>
);
}

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,623 @@
# jazz-example-pets
## 0.0.84
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-browser-media-images@0.7.19
- jazz-react@0.7.19
## 0.0.83
### Patch Changes
- jazz-react@0.7.18
- jazz-tools@0.7.18
- jazz-browser-media-images@0.7.18
## 0.0.82
### Patch Changes
- jazz-react@0.7.17
- jazz-tools@0.7.17
- jazz-browser-media-images@0.7.17
## 0.0.81
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-browser-media-images@0.7.16
- jazz-react@0.7.16
## 0.0.80
### Patch Changes
- Updated dependencies
- jazz-react@0.7.15
## 0.0.79
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.14
- jazz-react@0.7.14
- jazz-browser-media-images@0.7.14
## 0.0.78
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.13
- jazz-browser-media-images@0.7.13
- jazz-react@0.7.13
## 0.0.77
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.12
- jazz-browser-media-images@0.7.12
- jazz-react@0.7.12
## 0.0.76
### Patch Changes
- jazz-react@0.7.11
- jazz-tools@0.7.11
- jazz-browser-media-images@0.7.11
## 0.0.75
### Patch Changes
- jazz-react@0.7.10
- jazz-tools@0.7.10
- jazz-browser-media-images@0.7.10
## 0.0.74
### Patch Changes
- jazz-react@0.7.9
- jazz-tools@0.7.9
- jazz-browser-media-images@0.7.9
## 0.0.73
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.8
- jazz-browser-media-images@0.7.8
- jazz-react@0.7.8
## 0.0.72
### Patch Changes
- Updated dependencies [9fdc91c]
- jazz-react@0.7.7
## 0.0.71
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.6
- jazz-browser-media-images@0.7.6
- jazz-react@0.7.6
## 0.0.70
### Patch Changes
- Updated dependencies
- jazz-react@0.7.5
- jazz-browser-media-images@0.7.5
## 0.0.69
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.68
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-browser-media-images@0.7.3
- jazz-react@0.7.3
## 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.84",
"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,59 @@
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(this: PetAccount, creationProps?: { name: string }) {
super.migrate(creationProps);
if (!this._refs.root) {
this.root = PetAccountRoot.create(
{
posts: ListOfPosts.create([], { owner: this }),
},
{ owner: this },
);
}
}
}
/** 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 loading={<div>Loading</div>}>
<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,558 @@
# jazz-example-todo
## 0.0.83
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-react@0.7.19
## 0.0.82
### Patch Changes
- jazz-react@0.7.18
- jazz-tools@0.7.18
## 0.0.81
### Patch Changes
- jazz-react@0.7.17
- jazz-tools@0.7.17
## 0.0.80
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-react@0.7.16
## 0.0.79
### Patch Changes
- Updated dependencies
- jazz-react@0.7.15
## 0.0.78
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.14
- jazz-react@0.7.14
## 0.0.77
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.13
- jazz-react@0.7.13
## 0.0.76
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.12
- jazz-react@0.7.12
## 0.0.75
### Patch Changes
- jazz-react@0.7.11
- jazz-tools@0.7.11
## 0.0.74
### Patch Changes
- jazz-react@0.7.10
- jazz-tools@0.7.10
## 0.0.73
### Patch Changes
- jazz-react@0.7.9
- jazz-tools@0.7.9
## 0.0.72
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.8
- jazz-react@0.7.8
## 0.0.71
### Patch Changes
- Updated dependencies [9fdc91c]
- jazz-react@0.7.7
## 0.0.70
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.6
- jazz-react@0.7.6
## 0.0.69
### Patch Changes
- Updated dependencies
- jazz-react@0.7.5
## 0.0.68
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.67
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-react@0.7.3
## 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.83",
"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(this: TodoAccount, 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,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);

View File

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

View File

@@ -1,65 +1,11 @@
FROM node:18-alpine AS base
FROM node:18-slim
# 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
COPY node_modules ./node_modules
COPY homepage/.next/standalone ./homepage
COPY homepage/.next/static ./homepage/.next/static
# 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
EXPOSE 3001
ENV PORT 3001
# 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"]
CMD ["node", "homepage/server.js"]

View File

@@ -1,248 +0,0 @@
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { fmtPrice, pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Jazz Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz 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>
<Grid>
<GridCard>
### Mesh Free
<span className="text-2xl">$0</span>
- Unlimited projects
- For individual developers
- Low-latency sync
- 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>/developer/mo
- Unlimited projects
- Up to 3 developers
- Low-latency sync
- Egress/mo: 50 million ops <span className="text-xs">or 500GB blobs</span>
- Storage: 25 million ops <span className="text-xs">or 250GB blobs</span>
<div className="text-xs">
- Extra egress: {fmtPrice(10 * pricePer1MtxSyncedOut)} per 10 million ops or 100GB blobs
- Extra storage: {fmtPrice(10 * pricePer1MtxStored)} per 10 million ops or 100GB blobs
</div>
</GridCard>
<GridCard>
### Mesh Pro <ComingSoonBadge/>
<span className="text-2xl">$19</span>/developer/mo
- Unlimited projects
- Up to 50 developers, SSO/SAML
- Ultra-low-latency sync
- Egress/mo: 100 million ops <span className="text-xs">or 1TB blobs</span>
- Storage: 50 million ops <span className="text-xs">or 500GB blobs</span>
<div className="text-xs">
- Extra egress: {fmtPrice(10 * pricePer1MtxSyncedOut)} per 10 million ops or 100GB blobs
- Extra storage: {fmtPrice(10 * pricePer1MtxStored)} per 10 million ops or 100GB blobs
</div>
</GridCard>
{/*<GridCard>
### Mesh Enterprise <ComingSoonBadge/>
<span className="text-2xl">Custom</span>
- Custom SLA
- Custom cloud deployment
- Dedicated support
- Audit logs
</GridCard>*/}
</Grid>
An operation represents an **individual user action**, or **10KB of data** for blobs/streams.
<Grid>
<GridItem className="col-start-1">
#### Egress:
<div className="text-sm">
- Operations sent out from Jazz Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each op should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Operations stored:
<div className="text-sm">
- Operations that are continuously persisted.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of ops generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- **Session A: 4 users co-oping 10 pages of text, typing them out as individual character inserts:**
- 3,000 inserts/page &times; 10 pages = 30,000 ops -> 30k ops stored
- 30,000 ops, each synced out to 3 other users -> 90k ops egress (one-time)
- **You could have ~50 such sessions/mo within Mesh Free** and you can keep storing ~80 such texts.
- **You could have ~500 such sessions/mo within Mesh Starter included usage** and you can keep storing ~800 such texts.
- **You could have ~1000 such sessions/mo within Mesh Pro included usage** and you can keep storing ~1600 such texts.
- Each further such session would cost you about {fmtPrice(90000 * pricePerTxSyncedOut)} and {fmtPrice(30000 * pricePerTxStored)} per month to keep storing the text.
- **Session B: 3 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 5 hours**
- 3 users &times; 10 FPS &times; 10s/min &times; 60min/h &times; 5h = 90k ops -> 90k ops stored
- 90k ops, each synced out to 2 other users -> 180k ops egress (one-time)
- **You could have ~20 such sessions/mo within Mesh Free** and you can keep storing 20 such canvases.
- **You could have ~250 such sessions/mo within Mesh Starter included usage** and you can keep storing 250 such canvases.
- **You could have ~500 such sessions/mo within Mesh Pro included usage** and you can keep storing 500 such canvases.
- Each further such session would cost you about {fmtPrice(180000 * pricePerTxSyncedOut)} and {fmtPrice(90000 * pricePerTxStored)} per month to keep storing the canvas.
- **Session C: A livestreamer streaming video (1GB total) to 25 viewers (combined live & on-demand)**
- 1GB = 100,000 ops (10KB each) -> 100k ops stored
- 100,000 ops, each synced out to 25 viewers -> 2.5M ops egress (one-time)
- **You could have ~2 such livestreams/mo within Mesh Free** and you can keep storing 25 such videos.
- **You could have ~20 such livestreams/mo within Mesh Starter included usage** and you can keep storing 250 such videos.
- **You could have ~40 such livestreams/mo within Mesh Pro included usage** and you can keep storing 500 such videos.
- Each further such livestream would cost you about {fmtPrice(2500000 * pricePerTxSyncedOut)} and {fmtPrice(100000 * pricePerTxStored)} per month to keep storing the video.
</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 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.
- Every op is additionally synced to your backup node and counted as synced out.
</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.
- Every op 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,268 +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, databases and message queues with **a single new abstraction: collaborative data**.
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>
## 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 & other CoValues
</div>
</GridCard>
<GridCard>
### `CoList`
<div className="text-sm">
- Collaborative ordered list
- Possible items:
- Immutable JSON & 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 & 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>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
- 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,108 +0,0 @@
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",
].join(" ")}
>
{props.children}
</div>
);
}
export function Grid({
className,
children,
}: {
className?: string;
children: ReactNode;
}) {
return (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
"mt-10 items-stretch",
className
)}
>
{children}
</div>
);
}
export function GridItem(props: { children: ReactNode; className?: string }) {
return <div className={props.className || ""}>{props.children}</div>;
}
export function GridFeature(props: {
icon: ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<div
className={[
"p-4 flex items-center gap-2",
"not-prose text-base",
"border border-stone-200 dark:border-stone-800 rounded-xl shadow-sm",
props.className || "",
].join(" ")}
>
<div className="text-stone-500 mr-2">{props.icon}</div>
{props.children}
</div>
);
}
export function GridCard(props: { children: ReactNode; className?: string }) {
return (
<div
className={[
"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(" ")}
>
{props.children}
</div>
);
}
export function MultiplayerIcon() {
return (
<div className="w-8 h-8 -my-1 -mr-2 relative z-0">
<MousePointer2Icon
size="20"
absoluteStrokeWidth
strokeWidth={2}
className="absolute top-1 right-0"
/>
<MousePointer2Icon
size="16"
absoluteStrokeWidth
strokeWidth={2}
className="absolute bottom-1 left-0 -scale-x-100"
/>
</div>
);
}
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}
</span>
);
}
import { IframeHTMLAttributes, ReactNode } from "react";
import { ResponsiveIframe as ResponsiveIframeClient } from "./ResponsiveIframe";
import { HandIcon, MousePointer2Icon, TextCursorIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export function ResponsiveIframe(
props: IframeHTMLAttributes<HTMLIFrameElement>
) {
return <ResponsiveIframeClient {...props} />;
}

View File

@@ -1,93 +0,0 @@
import { ComingSoonBadge, Grid, GridCard, GridItem } from "./forMdx";
export const pricePer1MtxSyncedOut = 0.1;
export const pricePer1MtxStored = 0.2;
export const pricePerTxSyncedOut = pricePer1MtxSyncedOut / 1_000_000;
export const pricePerTxStored = pricePer1MtxStored / 1_000_000;
export function fmtPrice(raw: number) {
return raw.toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumSignificantDigits: 2,
});
}
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

@@ -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,670 @@
import { Slogan } from "@/components/forMdx";
import { JazzLogo } from "@/components/logos";
<h1 id="guide">Learn some <JazzLogo className="h-[1.3em] relative -top-0.5 inline-block -ml-[0.1em] -mr-[0.1em]"/></h1>
<Slogan>Build an issue tracker 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="guide-setup">Project Setup</h2>
1. Create a project called "circular" from a generic Vite starter template:
{/* prettier-ignore */}
```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>
{/* prettier-ignore */}
```bash
cd circular
npm install jazz-tools jazz-react
```
3. Modify `src/main.tsx` to set up a Jazz context:
{/* prettier-ignore */}
```tsx
import React from "react"; // old
import ReactDOM from "react-dom/client"; // old
import App from "./App.tsx"; // old
import "./index.css"; // old
import { createJazzReactContext, DemoAuth } from "jazz-react";
// old
const Jazz = createJazzReactContext({
auth: DemoAuth({ appName: "Circular" }),
peer: "wss://mesh.jazz.tools/?key=you@example.com", // <- put your email here to get a proper API key later
});
export const { useAccount, useCoState } = Jazz;
// old
ReactDOM.createRoot(document.getElementById("root")!).render( // old
<Jazz.Provider>
<React.StrictMode> // old
{" "}// old
<App /> // old
</React.StrictMode>{" "}// old
</Jazz.Provider>,
); // old
```
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="declaring-covalues">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 id="reading-covalues">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:
{/* prettier-ignore */}
```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 id="creating-covalues">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:
{/* prettier-ignore */}
```tsx
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue.tsx";
// old
function App() {// old
const [issue, setIssue] = useState<Issue>();
// old
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button>Create Issue</button>;
}
} // old
// old
export default App; // old
```
Now, finally, let's implement creating an issue:
{/* prettier-ignore */}
```tsx
import { useState } from "react"; // old
import { Issue } from "./schema"; // old
import { IssueComponent } from "./components/Issue.tsx"; // old
import { useAccount } from "./main";
// old
function App() {// old
const { me } = useAccount();
const [issue, setIssue] = useState<Issue>(); // old
// old
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);
};
// old
if (issue) {// old
return <IssueComponent issue={issue} />; // old
} else { // old
return <button onClick={createIssue}>Create Issue</button>;
} // old
} // old
// old
export default App; // old
```
🏁 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 -mb-3">
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 id="editing-and-subscription">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`:
{/* prettier-ignore */}
```tsx
import { useState } from "react"; // old
import { Issue } from "./schema"; // old
import { IssueComponent } from "./components/Issue.tsx"; // old
import { useAccount, useCoState } from "./main";
import { ID } from "jazz-tools"
// old
function App() { // old
const { me } = useAccount(); // old
const [issueID, setIssueID] = useState<ID<Issue>>();
// old
const issue = useCoState(Issue, issueID);
// old
const createIssue = () => {// old
const newIssue = Issue.create(// old
{ // old
title: "Buy terrarium", // old
description: "Make sure it's big enough for 10 snails.", // old
estimate: 5, // old
status: "backlog", // old
}, // old
{ owner: me }, // old
); // old
setIssueID(newIssue.id);
}; // old
// old
if (issue) { // old
return <IssueComponent issue={issue} />; // old
} else { // old
return <button onClick={createIssue}>Create Issue</button>; // old
} // old
} // old
// old
export default App; // old
```
And now for the exciting part! Let's make `src/components/Issue.tsx` an editing component.
{/* prettier-ignore */}
```tsx
import { Issue } from "../schema"; // old
// old
export function IssueComponent({ issue }: { issue: Issue }) { // old
return ( // old
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t"> // old
<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</option>
</select>
</div> // old
); // old
} // old
```
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider -mb-3">
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>
🏁 Now you should be able to edit the issue after creating it!
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)
- notify subscribers of the change (who will receive a fresh, updated view of the CoValue)
<aside className="text-sm border border-stone-300 dark:border-stone-700 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 border-stone-300 dark:border-stone-700">💡 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, 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, 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 id="persistence">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.**
Actually, all the Issue CoValues we've created so far **have already been automatically persisted** to the cloud and locally - but we loose track of their ID after a reload.
So let's store the ID in window URL state and make sure our useState is in sync with that.
{/* prettier-ignore */}
```tsx
import { useState } from "react"; // old
import { Issue } from "./schema"; // old
import { IssueComponent } from "./components/Issue.tsx"; // old
import { useAccount, useCoState } from "./main"; // old
import { ID } from "jazz-tools" // old
// old
function App() { // old
const { me } = useAccount(); // old
const [issueID, setIssueID] = useState<ID<Issue> | undefined>(
(window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,
);
// old
const issue = useCoState(Issue, issueID); // old
// old
const createIssue = () => {// old
const newIssue = Issue.create(// old
{ // old
title: "Buy terrarium", // old
description: "Make sure it's big enough for 10 snails.", // old
estimate: 5, // old
status: "backlog", // old
}, // old
{ owner: me }, // old
); // old
setIssueID(newIssue.id); // old
window.history.pushState({}, "", `?issue=${newIssue.id}`);
}; // old
// old
if (issue) { // old
return <IssueComponent issue={issue} />; // old
} else { // old
return <button onClick={createIssue}>Create Issue</button>; // old
} // old
} // old
// old
export default App; // old
```
🏁 Now you should be able to create an issue, edit it, reload the page, and still see the same issue.
<h3 id="remote-sync">Remote sync</h3>
To see that sync is also already working, try the following:
- copy the URL to a new tab in the same browser window and see the same issue
- edit the issue and see the changes reflected in the other tab!
This works because we load the issue as the same account that created it and owns it (remember setting `{ owner: me }`?).
We'll learn more about access control in "Groups & Permissions", but for now let's build a super simple way of sharing an Issue by just making it publicly readable & writable.
All we have to do is create a new group to own each new issue and add "everyone" as a "writer":
{/* prettier-ignore */}
```tsx
import { useState } from "react"; // old
import { Issue } from "./schema"; // old
import { IssueComponent } from "./components/Issue.tsx"; // old
import { useAccount, useCoState } from "./main"; // old
import { ID, Group } from "jazz-tools"
// old
function App() { // old
const { me } = useAccount(); // old
const [issueID, setIssueID] = useState<ID<Issue> | undefined>(// old
(window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,// old
); // old
// old
const issue = useCoState(Issue, issueID); // old
// old
const createIssue = () => { // old
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
// old
const newIssue = Issue.create( // old
{ // old
title: "Buy terrarium", // old
description: "Make sure it's big enough for 10 snails.", // old
estimate: 5, // old
status: "backlog", // old
}, // old
{ owner: group },
); // old
setIssueID(newIssue.id); // old
window.history.pushState({}, "", `?issue=${newIssue.id}`); // old
}; // old
// old
if (issue) { // old
return <IssueComponent issue={issue} />; // old
} else { // old
return <button onClick={createIssue}>Create Issue</button>; // old
} // old
} // old
// old
export default App; // old
```
🏁 Now you should be able to open the Issue (with its unique URL) on another device or browser, or send it to a friend and you should be able to **edit it together in realtime!**
This concludes our intro to the essence of CoValues. Hopefully you're starting to have a feeling for how CoValues behave and how they're magically available everywhere.
<h2 id="refs-and-on-demand-subscribe">Refs & Auto-Subscribe</h2>
Now let's have a look at how to compose CoValues into more complex structures and build a whole app around them.
Let's extend our two data model to include "Projects" which have a list of tasks and some properties of their own.
Using plain objects, you would probably type a Project like this:
```ts
type Project = {
name: string;
issues: Issue[];
};
```
In order to create this more complex structure in a fully collaborative way, we're going to need _references_ that allow us to nest or link CoValues.
Add the following to `src/schema.ts`:
```ts
import { CoMap, CoList, co } from "jazz-tools";
// old
export class Issue extends CoMap { // old
title = co.string; // old
description = co.string; // old
estimate = co.number; // old
status? = co.literal("backlog", "in progress", "done"); // old
} // old
// old
export class ListOfIssues extends CoList.Of(co.ref(Issue)) {}
export class Project extends CoMap {
name = co.string;
issues = co.ref(ListOfIssues);
}
```
Now let's change things up a bit in terms of components as well.
First, we'll change `App.tsx` to create and render `Project`s instead of `Issue`s. (We'll move the `useCoState` into the `ProjectComponent` we'll create in a second).
{/* prettier-ignore */}
```tsx
import { useState } from "react"; // old
import { Project, ListOfIssues } from "./schema";
import { ProjectComponent } from "./components/Project.tsx";
import { useAccount } from "./main";
import { ID, Group } from "jazz-tools"
// old
function App() { // old
const { me } = useAccount(); // old
const [projectID, setProjectID] = useState<ID<Project> | undefined>(
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined,// old
);
// old
const issue = useCoState(Issue, issueID); // *bin*
// old
const createProject = () => {
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
const newProject = Project.create(
{
name: "New Project",
issues: ListOfIssues.create([], { owner: group })
},
{ owner: group },
);
setProjectID(newProject.id);
window.history.pushState({}, "", `?project=${newProject.id}`);
};
// old
if (projectID) {
return <ProjectComponent projectID={projectID} />;
} else {
return <button onClick={createProject}>Create Project</button>;
}
} // old
// old
export default App; // old
```
Now we'll actually create the `ProjectComponent` that renders a `Project` and its `Issue`s.
Create a new file `src/components/Project.tsx` and add the following:
{/* prettier-ignore */}
```tsx
import { ID } from "jazz-tools";
import { Project, Issue } from "../schema";
import { IssueComponent } from "./Issue.tsx";
import { useCoState } from "../main";
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
const project = useCoState(Project, projectID);
const createAndAddIssue = () => {
project?.issues?.push(Issue.create({
title: "",
description: "",
estimate: 0,
status: "backlog",
}, { owner: project._owner }));
};
return project ? (
<div>
<h1>{project.name}</h1>
<div className="border-r border-b">
{project.issues?.map((issue) => (
issue && <IssueComponent key={issue.id} issue={issue} />
))}
<button onClick={createAndAddIssue}>Create Issue</button>
</div>
</div>
) : (
<div>Loading project...</div>
);
}
```
🏁 Now you should be able to create a project, add issues to it, share it, and edit it collaboratively!
Two things to note here:
- We create a new Issue like before, and then push it into the `issues` list of the Project. By setting the `owner` to the Project's owner, we ensure that the Issue has the same access rights as the project itself.
- We only need to use `useCoState` on the Project, and the nested `ListOfIssues` and each `Issue` will be **automatically loaded and subscribed to when we access them.**
- However, because either the `Project`, `ListOfIssues`, or each `Issue` might not be loaded yet, we have to check for them being defined.
The load-and-subscribe-on-access is a convenient way to have your rendering drive data loading (including in nested components!) and lets you quickly chuck UIs together without worrying too much about the shape of all data you'll need.
But you can also take more precise control over loading by defining a minimum-depth to load in `useCoState`:
{/* prettier-ignore */}
```tsx
import { ID } from "jazz-tools";// old
import { Project, Issue } from "../schema"; // old
import { IssueComponent } from "./Issue.tsx"; // old
import { useCoState } from "../main"; // old
// old
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
const project = useCoState(Project, projectID, { issues: [{}] });
const createAndAddIssue = () => {// old
project?.issues.push(Issue.create({
title: "",// old
description: "",// old
estimate: 0,// old
status: "backlog",// old
}, { owner: project._owner }));// old
};// old
// old
return project ? (// old
<div>// old
<h1>{project.name}</h1>// old
<div className="border-r border-b">// old
{project.issues.map((issue) => (
<IssueComponent key={issue.id} issue={issue} />
))}// old
<button onClick={createAndAddIssue}>Create Issue</button>// old
</div>// old
</div>// old
) : (// old
<div>Loading project...</div>// old
);// old
}// old
```
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
- Now, we can get rid of a lot of coniditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
TODO: explainer about not loaded vs not set/defined and `_refs` basics
<div className="text-amber-500 mt-52">
🚧 OH NO - This is as far as we've written the Guide. 🚧
</div>
{" -> "}
<a href="https://github.com/gardencmp/jazz/issues/186">Complain on GitHub</a>
<h2 id="groups-and-permissions">Groups & Permissions</h2>
<h2 id="auth-accounts-and-migrations">Auth, Accounts & Migrations</h2>
<h2 id="edits-and-time-travel">Edit Metadata & Time Travel</h2>
<h2 id="backend-workers">Backend Workers</h2>

View File

@@ -0,0 +1,62 @@
import { DocNav } from "@/components/docs/nav";
import { PackageDocs } from "@/components/docs/packageDocs";
import Guide from "./guide.mdx";
import { Prose } from "@/components/forMdx";
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 text-stone-700 dark:text-stone-300 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] text-base">
<Prose className="prose">
<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>
</Prose>
<div className="xl:-mr-[calc(50vw-40rem)]">
<Prose>
<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>
</Prose>
<div className="text-stone-800 dark:text-stone-200">
<PackageDocs package="jazz-tools" />
<PackageDocs package="jazz-react" />
<PackageDocs package="jazz-browser" />
<PackageDocs package="jazz-browser-media-images" />
<PackageDocs package="jazz-nodejs" />
</div>
</div>
</div>
</>
);
}

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