Compare commits
345 Commits
jazz-react
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34dda7bdbd | ||
|
|
49fa153581 | ||
|
|
c80b827775 | ||
|
|
a2bf9f988a | ||
|
|
ac27b2d5c2 | ||
|
|
c813518fdc | ||
|
|
d5034ed5c3 | ||
|
|
cf2c29a365 | ||
|
|
d948823db6 | ||
|
|
060ad4630d | ||
|
|
0ddceac4c0 | ||
|
|
a862cb8819 | ||
|
|
4246aed7db | ||
|
|
41554e0e0b | ||
|
|
93c4d8155e | ||
|
|
24eefd49f1 | ||
|
|
e712f1e8ef | ||
|
|
33db0fd654 | ||
|
|
478ded93de | ||
|
|
89ad1fb79d | ||
|
|
1ba40806ec | ||
|
|
73ae281e4a | ||
|
|
a35353c987 | ||
|
|
1cb91003cc | ||
|
|
d850022491 | ||
|
|
93792ab6f6 | ||
|
|
95dfe7af6a | ||
|
|
734258eb17 | ||
|
|
f3bcf96fad | ||
|
|
5cf0bc1911 | ||
|
|
d32a6b275f | ||
|
|
6caba9f8e7 | ||
|
|
641f1dbfbe | ||
|
|
58d9a104d6 | ||
|
|
7b9d24c8ef | ||
|
|
4225fdd537 | ||
|
|
9fdc91c6de | ||
|
|
93d8c85e5c | ||
|
|
929cddc3c3 | ||
|
|
e0bc63f016 | ||
|
|
b29ac306ea | ||
|
|
e8e883f4d6 | ||
|
|
3325ff1cd6 | ||
|
|
4fe14f03b4 | ||
|
|
90e2a661e4 | ||
|
|
6ed53ecb79 | ||
|
|
c18775766c | ||
|
|
4bb3a6209a | ||
|
|
0f44a547a4 | ||
|
|
1e2f6d8f14 | ||
|
|
7e5b176930 | ||
|
|
b420eab503 | ||
|
|
b370e2e14e | ||
|
|
1fabee297d | ||
|
|
484dc460c5 | ||
|
|
0cb8756124 | ||
|
|
95d0f0221b | ||
|
|
0c9c0fcd60 | ||
|
|
8be0dd133c | ||
|
|
e68e0ada0d | ||
|
|
49a7349e4d | ||
|
|
979c7241a4 | ||
|
|
e011e4a049 | ||
|
|
92bccf5974 | ||
|
|
2c1d6dcb8f | ||
|
|
75f45ec0b2 | ||
|
|
c0ebcadda9 | ||
|
|
109e9b6a5b | ||
|
|
d0c3d08e42 | ||
|
|
7514830edb | ||
|
|
6f27128b87 | ||
|
|
376518d4ef | ||
|
|
272fc85c13 | ||
|
|
579e4f93ee | ||
|
|
a9accdc3a8 | ||
|
|
b403db51d4 | ||
|
|
108cae037f | ||
|
|
c51897ab9e | ||
|
|
67171cb07a | ||
|
|
6ae3bf6ac9 | ||
|
|
b64b15877a | ||
|
|
19f52b7361 | ||
|
|
9dcd15dbc8 | ||
|
|
a423eeea3b | ||
|
|
99be6a3566 | ||
|
|
e97f730c0f | ||
|
|
f5642335ff | ||
|
|
bf0f8ec824 | ||
|
|
fb9ef4ea20 | ||
|
|
86363197cd | ||
|
|
e93187c971 | ||
|
|
1a86e13cf1 | ||
|
|
b863c1d20a | ||
|
|
4c8d658c25 | ||
|
|
69d37437ef | ||
|
|
220bdbae62 | ||
|
|
b23b556b79 | ||
|
|
ce721cf3d1 | ||
|
|
41363415fe | ||
|
|
b91d0769d5 | ||
|
|
ad16690826 | ||
|
|
ca7b81c36a | ||
|
|
a632ce1477 | ||
|
|
ca7011e9af | ||
|
|
34df432ee8 | ||
|
|
dfa7178041 | ||
|
|
a3e9a3b686 | ||
|
|
599db049f2 | ||
|
|
7cba6dd690 | ||
|
|
2d406c9d58 | ||
|
|
d8fe2b10f1 | ||
|
|
c8343626ba | ||
|
|
5b188ec093 | ||
|
|
7ebbd80049 | ||
|
|
74c9e5d36d | ||
|
|
1a3530747f | ||
|
|
79899b9b18 | ||
|
|
6ef6a2f507 | ||
|
|
ab6d15c9f7 | ||
|
|
a0dc9139a2 | ||
|
|
220fa319d5 | ||
|
|
183d505a5e | ||
|
|
f960e7e736 | ||
|
|
d68ac84e03 | ||
|
|
30fbe2b6d7 | ||
|
|
e3a00570e1 | ||
|
|
499e02685a | ||
|
|
6b0418f772 | ||
|
|
1200aae47d | ||
|
|
20fdb09b33 | ||
|
|
522b12dc42 | ||
|
|
a7cd0dcce5 | ||
|
|
fd86c11336 | ||
|
|
4fc414d744 | ||
|
|
6d49e9b06c | ||
|
|
8b866e288b | ||
|
|
bf22588b0e | ||
|
|
60d5ca2811 | ||
|
|
9e5dcdfa69 | ||
|
|
3cc39dd5ed | ||
|
|
6878060346 | ||
|
|
9840061137 | ||
|
|
6f84e00463 | ||
|
|
1e82d0d34e | ||
|
|
f35bc468b3 | ||
|
|
719071c286 | ||
|
|
c4b439e2e6 | ||
|
|
77c2b56ceb | ||
|
|
0b17b7ad5a | ||
|
|
db3011a1c9 | ||
|
|
b47c695b97 | ||
|
|
55c1c893ba | ||
|
|
bde684fe30 | ||
|
|
89b6c9004b | ||
|
|
97cdfbddaf | ||
|
|
584ee2d136 | ||
|
|
21771c4725 | ||
|
|
7e8f1bed15 | ||
|
|
226ae03603 | ||
|
|
96c494f5ee | ||
|
|
23ba00422f | ||
|
|
52675c9c68 | ||
|
|
1b113e0114 | ||
|
|
0a930f5eeb | ||
|
|
84f5a83648 | ||
|
|
5fa277c254 | ||
|
|
d49c7f2dd4 | ||
|
|
a78f1688d9 | ||
|
|
cd37a846d8 | ||
|
|
63374ccb6d | ||
|
|
efe2d91fb3 | ||
|
|
234b2a019b | ||
|
|
4bbcd366bc | ||
|
|
5724f8747a | ||
|
|
38d44103d1 | ||
|
|
96a7ff68e7 | ||
|
|
fb78a55f76 | ||
|
|
fdc7fc7bcf | ||
|
|
ed5643aaf1 | ||
|
|
ac431ef9ef | ||
|
|
0940508637 | ||
|
|
704af7d04c | ||
|
|
e4e476a834 | ||
|
|
ece35b3c6f | ||
|
|
b26eab50b3 | ||
|
|
b42313a285 | ||
|
|
129e2c1668 | ||
|
|
87ddb81562 | ||
|
|
daee49cd9d | ||
|
|
3aaf773b0a | ||
|
|
460478fc65 | ||
|
|
fe8b5f45b9 | ||
|
|
01ac646c8e | ||
|
|
d4b9fbcc60 | ||
|
|
1cfa279543 | ||
|
|
e35be73bcc | ||
|
|
f8a5c46e18 | ||
|
|
1c7d85ce76 | ||
|
|
19004b4c36 | ||
|
|
930fa689a7 | ||
|
|
18a7b2d6b4 | ||
|
|
d2e03ff9d3 | ||
|
|
77a9c8395e | ||
|
|
c4151fcb95 | ||
|
|
4c5c21bba2 | ||
|
|
f0f6f1b71c | ||
|
|
a9d6d5a1db | ||
|
|
7849ce6de7 | ||
|
|
354bdcdbfb | ||
|
|
8ecd3e88c8 | ||
|
|
85d2b627f1 | ||
|
|
88fd92e4dc | ||
|
|
952982e7ea | ||
|
|
22e7c27af7 | ||
|
|
59c18c34de | ||
|
|
6acbaede44 | ||
|
|
1a44f875b3 | ||
|
|
9d935fe1d0 | ||
|
|
e5eed5b9b7 | ||
|
|
05a549f04f | ||
|
|
a5e68a4fae | ||
|
|
016a9e342a | ||
|
|
627d8950ae | ||
|
|
770ce08c10 | ||
|
|
69ac514b3b | ||
|
|
b1481748f9 | ||
|
|
49944e323f | ||
|
|
15310db389 | ||
|
|
ea5c5a2604 | ||
|
|
e461dd1355 | ||
|
|
e299c3e9d8 | ||
|
|
406c47271f | ||
|
|
05c7efea85 | ||
|
|
ce7ddf7055 | ||
|
|
beb40b5db6 | ||
|
|
2def752cc4 | ||
|
|
bacf3ae86a | ||
|
|
0fef382f2e | ||
|
|
95523d8538 | ||
|
|
71f7220bfd | ||
|
|
2212c6deac | ||
|
|
fb3efe4cfd | ||
|
|
e66ac6a7d0 | ||
|
|
7ab3908848 | ||
|
|
921f1fbfe8 | ||
|
|
2ac455f8b5 | ||
|
|
1ce881aed2 | ||
|
|
b1b5140951 | ||
|
|
b109c23233 | ||
|
|
a7a34a0b6e | ||
|
|
4bf63934e1 | ||
|
|
16f572282f | ||
|
|
44380c3700 | ||
|
|
dc46cb1386 | ||
|
|
3ccb1e8ad7 | ||
|
|
d973c5f48b | ||
|
|
f4af78c834 | ||
|
|
e6d323fd30 | ||
|
|
e6ab56aeb5 | ||
|
|
779765b649 | ||
|
|
6da730779a | ||
|
|
a3e77edc57 | ||
|
|
ed00308986 | ||
|
|
89e9092e0f | ||
|
|
f8b11754c8 | ||
|
|
4b38d0793c | ||
|
|
b2156f8154 | ||
|
|
3a5422e635 | ||
|
|
54d3d76868 | ||
|
|
f4dc0ec1b7 | ||
|
|
f500db2dd3 | ||
|
|
95f64f9934 | ||
|
|
cccb0e1a21 | ||
|
|
b434a4227f | ||
|
|
6ba4dc1f04 | ||
|
|
2fe4c81d1e | ||
|
|
5c00264184 | ||
|
|
c744849c9b | ||
|
|
f59b278f00 | ||
|
|
b26c155d5f | ||
|
|
6da79b8745 | ||
|
|
0b92591b17 | ||
|
|
974456db54 | ||
|
|
a1326a80fe | ||
|
|
00d6946b24 | ||
|
|
c4ffde93c0 | ||
|
|
37bfe967ea | ||
|
|
9abbbfd6fb | ||
|
|
155cd08e39 | ||
|
|
e2e6bdf3bd | ||
|
|
810c42c743 | ||
|
|
99e4c1301e | ||
|
|
8c86a831fc | ||
|
|
5e976416a4 | ||
|
|
0339e14260 | ||
|
|
4b94fcebf1 | ||
|
|
ddd2a79f37 | ||
|
|
01a8f2dab3 | ||
|
|
801629d2c1 | ||
|
|
87d62c941f | ||
|
|
7e6e0fdcc5 | ||
|
|
a73b07424c | ||
|
|
0f9b983132 | ||
|
|
43e25902d3 | ||
|
|
2c27c8517f | ||
|
|
b496058a0e | ||
|
|
4313663bd1 | ||
|
|
dbdbfbd07a | ||
|
|
184b23d61f | ||
|
|
5c03b4f668 | ||
|
|
bdbe777d68 | ||
|
|
a838a18647 | ||
|
|
3f5a664ee7 | ||
|
|
707292e1ff | ||
|
|
9a81b63943 | ||
|
|
30216b7b80 | ||
|
|
b2fc91c2ce | ||
|
|
ef0328833c | ||
|
|
6a93f17a4a | ||
|
|
01bd07ac66 | ||
|
|
88859cfeca | ||
|
|
dfe563e2bc | ||
|
|
7fc0ff981d | ||
|
|
1a9132102d | ||
|
|
d39638282f | ||
|
|
4354c340fc | ||
|
|
c3a97b29a9 | ||
|
|
b65e30ec70 | ||
|
|
23a1e0266a | ||
|
|
76acecfe50 | ||
|
|
5031c77afb | ||
|
|
af90b8c989 | ||
|
|
d06b4adad0 | ||
|
|
b961cde946 | ||
|
|
8cbbe2f312 | ||
|
|
c15a49d82d | ||
|
|
93809911de | ||
|
|
edeb2ca9f4 | ||
|
|
01662fc3b8 | ||
|
|
134d2f0fda | ||
|
|
142973827c | ||
|
|
47444888c3 | ||
|
|
a4769058f4 | ||
|
|
f7f091e18c | ||
|
|
a969430247 |
@@ -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": []
|
||||
}
|
||||
|
||||
43
.github/workflows/build-and-deploy.yaml
vendored
43
.github/workflows/build-and-deploy.yaml
vendored
@@ -11,18 +11,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop"]
|
||||
example: ["chat", "pets", "todo", "inspector"]
|
||||
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
@@ -34,14 +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
|
||||
@@ -61,6 +61,15 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
@@ -71,6 +80,17 @@ jobs:
|
||||
username: gardencmp
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pnpm Install (root)
|
||||
run: |
|
||||
pnpm install
|
||||
working-directory: .
|
||||
|
||||
- name: Pnpm Install & Build (homepage)
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm build;
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- name: Docker Build & Push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -85,7 +105,8 @@ jobs:
|
||||
needs: build-examples
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop"]
|
||||
example: ["chat", "pets", "todo", "inspector"]
|
||||
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
node_modules
|
||||
yarn-error.log
|
||||
lerna-debug.log
|
||||
docsTmp
|
||||
docsTmp
|
||||
.DS_Store
|
||||
.turbo
|
||||
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
@@ -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
118
README.md
@@ -1,116 +1,12 @@
|
||||
# Jazz - instant sync
|
||||
|
||||
<sub>Homepage: [jazz.tools](https://jazz.tools) — Docs: [DOCS.md](./DOCS.md) — Community & support: [Discord](https://discord.gg/utDMjHYg42) — 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** — 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 — 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 — Garden Computing, Inc.
|
||||
Copyright 2024 — Garden Computing, Inc.
|
||||
@@ -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).
|
||||
@@ -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: {},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,582 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.46",
|
||||
"version": "0.0.64",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
42
examples/chat/src/chatScreen.tsx
Normal file
42
examples/chat/src/chatScreen.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ID } from 'jazz-tools';
|
||||
import { Chat, Message, useCoState } from './app.tsx';
|
||||
|
||||
export function ChatScreen(props: { chatID: ID<Chat> }) {
|
||||
const chat = useCoState(Chat, props.chatID, [{}]);
|
||||
|
||||
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
|
||||
{chat.length > 0
|
||||
? chat.map((msg) => <ChatBubble msg={msg} key={msg.id} />)
|
||||
: <div className='m-auto text-sm'>(Empty chat)</div>}
|
||||
<ChatInput onSubmit={(text) => {
|
||||
chat.push(
|
||||
Message.create({ text }, { owner: chat._owner })
|
||||
);
|
||||
}} />
|
||||
</div> : <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function ChatBubble(props: { msg: Message }) {
|
||||
const lastEdit = props.msg._edits.text;
|
||||
const align = lastEdit.by?.isMe ? 'items-end' : 'items-start';
|
||||
|
||||
return <div className={`${align} flex flex-col`}>
|
||||
<div className='rounded-xl bg-stone-100 dark:bg-stone-700 dark:text-white py-2 px-4 mt-2 min-w-[5rem]'>
|
||||
{ props.msg.text }
|
||||
</div>
|
||||
<div className='text-xs text-neutral-500 ml-2'>
|
||||
{ lastEdit.by?.profile?.name }{' '}
|
||||
{ lastEdit.madeAt?.toLocaleTimeString() }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ChatInput(props: { onSubmit: (text: string) => void }) {
|
||||
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white border-stone-300 dark:border-stone-700'
|
||||
placeholder='Type a message and press Enter'
|
||||
onKeyDown={({ key, currentTarget: input }) => {
|
||||
if (key !== 'Enter' || !input.value) return;
|
||||
props.onSubmit(input.value);
|
||||
input.value = '';
|
||||
}} />;
|
||||
}
|
||||
@@ -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 = '';
|
||||
}}/>
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { CoMap, CoList } from 'cojson';
|
||||
|
||||
export type Chat = CoList<Message['id']>;
|
||||
export type Message = CoMap<{ text: string }>;
|
||||
@@ -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
|
||||
@@ -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).
|
||||
26
examples/inspector/CHANGELOG.md
Normal file
26
examples/inspector/CHANGELOG.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 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
|
||||
42
examples/inspector/README.md
Normal file
42
examples/inspector/README.md
Normal 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).
|
||||
56
examples/inspector/job-template.nomad
Normal file
56
examples/inspector/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
||||
job "inspector$BRANCH_SUFFIX" {
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
to = 80
|
||||
}
|
||||
}
|
||||
|
||||
constraint {
|
||||
attribute = "${node.class}"
|
||||
operator = "="
|
||||
value = "mesh"
|
||||
}
|
||||
|
||||
spread {
|
||||
attribute = "${node.datacenter}"
|
||||
weight = 100
|
||||
}
|
||||
|
||||
constraint {
|
||||
distinct_hosts = true
|
||||
}
|
||||
|
||||
task "server" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "$DOCKER_TAG"
|
||||
ports = ["http"]
|
||||
|
||||
auth = {
|
||||
username = "$DOCKER_USER"
|
||||
password = "$DOCKER_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
name = "inspector$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 50 # MHz
|
||||
memory = 50 # MB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# deploy bump 4
|
||||
49
examples/inspector/package.json
Normal file
49
examples/inspector/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"private": true,
|
||||
"version": "0.0.48",
|
||||
"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.1.5",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
309
examples/inspector/src/app.tsx
Normal file
309
examples/inspector/src/app.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
249
examples/inspector/src/cojson-tree.tsx
Normal file
249
examples/inspector/src/cojson-tree.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
examples/inspector/src/link-icon.tsx
Normal file
18
examples/inspector/src/link-icon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
9
examples/pets/.prettierrc.js
Normal file
9
examples/pets/.prettierrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 4,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,10 +1,606 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.63",
|
||||
"version": "0.0.82",
|
||||
"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",
|
||||
|
||||
59
examples/pets/src/1_schema.ts
Normal file
59
examples/pets/src/1_schema.ts
Normal 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 */
|
||||
@@ -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 */
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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',
|
||||
|
||||
9
examples/todo/.prettierrc.js
Normal file
9
examples/todo/.prettierrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 4,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,9 +1,543 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.63",
|
||||
"version": "0.0.81",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "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",
|
||||
|
||||
55
examples/todo/src/1_schema.ts
Normal file
55
examples/todo/src/1_schema.ts
Normal 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 */
|
||||
@@ -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 */
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -13,4 +13,4 @@ export default defineConfig({
|
||||
build: {
|
||||
minify: false
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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).
|
||||
695
genDocsMd.ts
695
genDocsMd.ts
@@ -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")
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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 × 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 × 10 FPS × 10s/min × 60min/h × 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 — 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>
|
||||
@@ -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 — 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 data‑fetching</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 — 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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 {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} />;
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
3
homepage/homepage/.eslintrc.json
Normal file
3
homepage/homepage/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "prettier"]
|
||||
}
|
||||
@@ -33,3 +33,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
typedoc
|
||||
codeSamples
|
||||
9
homepage/homepage/.prettierrc.js
Normal file
9
homepage/homepage/.prettierrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 4,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -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!
|
||||
|
||||
670
homepage/homepage/app/docs/guide.mdx
Normal file
670
homepage/homepage/app/docs/guide.mdx
Normal 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... “Circular.”**</span>
|
||||
|
||||
We'll build everything **step-by-step,** in typical, immediately usable stages. We'll explore many important things Jazz does — 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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user