Compare commits
324 Commits
cojson@0.6
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2f6d8f14 | ||
|
|
7e5b176930 | ||
|
|
b420eab503 | ||
|
|
b370e2e14e | ||
|
|
1fabee297d | ||
|
|
484dc460c5 | ||
|
|
0cb8756124 | ||
|
|
95d0f0221b | ||
|
|
0c9c0fcd60 | ||
|
|
8be0dd133c | ||
|
|
e68e0ada0d | ||
|
|
49a7349e4d | ||
|
|
979c7241a4 | ||
|
|
e011e4a049 | ||
|
|
92bccf5974 | ||
|
|
2c1d6dcb8f | ||
|
|
75f45ec0b2 | ||
|
|
c0ebcadda9 | ||
|
|
109e9b6a5b | ||
|
|
d0c3d08e42 | ||
|
|
7514830edb | ||
|
|
6f27128b87 | ||
|
|
376518d4ef | ||
|
|
272fc85c13 | ||
|
|
579e4f93ee | ||
|
|
a9accdc3a8 | ||
|
|
b403db51d4 | ||
|
|
108cae037f | ||
|
|
c51897ab9e | ||
|
|
67171cb07a | ||
|
|
6ae3bf6ac9 | ||
|
|
b64b15877a | ||
|
|
19f52b7361 | ||
|
|
9dcd15dbc8 | ||
|
|
a423eeea3b | ||
|
|
99be6a3566 | ||
|
|
e97f730c0f | ||
|
|
f5642335ff | ||
|
|
bf0f8ec824 | ||
|
|
fb9ef4ea20 | ||
|
|
86363197cd | ||
|
|
e93187c971 | ||
|
|
1a86e13cf1 | ||
|
|
b863c1d20a | ||
|
|
4c8d658c25 | ||
|
|
69d37437ef | ||
|
|
220bdbae62 | ||
|
|
b23b556b79 | ||
|
|
ce721cf3d1 | ||
|
|
41363415fe | ||
|
|
b91d0769d5 | ||
|
|
ad16690826 | ||
|
|
ca7b81c36a | ||
|
|
a632ce1477 | ||
|
|
ca7011e9af | ||
|
|
34df432ee8 | ||
|
|
dfa7178041 | ||
|
|
a3e9a3b686 | ||
|
|
599db049f2 | ||
|
|
7cba6dd690 | ||
|
|
2d406c9d58 | ||
|
|
d8fe2b10f1 | ||
|
|
c8343626ba | ||
|
|
5b188ec093 | ||
|
|
7ebbd80049 | ||
|
|
74c9e5d36d | ||
|
|
1a3530747f | ||
|
|
79899b9b18 | ||
|
|
6ef6a2f507 | ||
|
|
ab6d15c9f7 | ||
|
|
a0dc9139a2 | ||
|
|
220fa319d5 | ||
|
|
183d505a5e | ||
|
|
f960e7e736 | ||
|
|
d68ac84e03 | ||
|
|
30fbe2b6d7 | ||
|
|
e3a00570e1 | ||
|
|
499e02685a | ||
|
|
6b0418f772 | ||
|
|
1200aae47d | ||
|
|
20fdb09b33 | ||
|
|
522b12dc42 | ||
|
|
a7cd0dcce5 | ||
|
|
fd86c11336 | ||
|
|
4fc414d744 | ||
|
|
6d49e9b06c | ||
|
|
8b866e288b | ||
|
|
bf22588b0e | ||
|
|
60d5ca2811 | ||
|
|
9e5dcdfa69 | ||
|
|
3cc39dd5ed | ||
|
|
6878060346 | ||
|
|
9840061137 | ||
|
|
6f84e00463 | ||
|
|
1e82d0d34e | ||
|
|
f35bc468b3 | ||
|
|
719071c286 | ||
|
|
c4b439e2e6 | ||
|
|
77c2b56ceb | ||
|
|
0b17b7ad5a | ||
|
|
db3011a1c9 | ||
|
|
b47c695b97 | ||
|
|
55c1c893ba | ||
|
|
bde684fe30 | ||
|
|
89b6c9004b | ||
|
|
97cdfbddaf | ||
|
|
584ee2d136 | ||
|
|
21771c4725 | ||
|
|
7e8f1bed15 | ||
|
|
226ae03603 | ||
|
|
96c494f5ee | ||
|
|
23ba00422f | ||
|
|
52675c9c68 | ||
|
|
1b113e0114 | ||
|
|
0a930f5eeb | ||
|
|
84f5a83648 | ||
|
|
5fa277c254 | ||
|
|
d49c7f2dd4 | ||
|
|
a78f1688d9 | ||
|
|
cd37a846d8 | ||
|
|
63374ccb6d | ||
|
|
efe2d91fb3 | ||
|
|
234b2a019b | ||
|
|
4bbcd366bc | ||
|
|
5724f8747a | ||
|
|
38d44103d1 | ||
|
|
96a7ff68e7 | ||
|
|
fb78a55f76 | ||
|
|
fdc7fc7bcf | ||
|
|
ed5643aaf1 | ||
|
|
ac431ef9ef | ||
|
|
0940508637 | ||
|
|
704af7d04c | ||
|
|
e4e476a834 | ||
|
|
ece35b3c6f | ||
|
|
b26eab50b3 | ||
|
|
b42313a285 | ||
|
|
129e2c1668 | ||
|
|
87ddb81562 | ||
|
|
daee49cd9d | ||
|
|
3aaf773b0a | ||
|
|
460478fc65 | ||
|
|
fe8b5f45b9 | ||
|
|
01ac646c8e | ||
|
|
d4b9fbcc60 | ||
|
|
1cfa279543 | ||
|
|
e35be73bcc | ||
|
|
f8a5c46e18 | ||
|
|
1c7d85ce76 | ||
|
|
19004b4c36 | ||
|
|
930fa689a7 | ||
|
|
18a7b2d6b4 | ||
|
|
d2e03ff9d3 | ||
|
|
77a9c8395e | ||
|
|
c4151fcb95 | ||
|
|
4c5c21bba2 | ||
|
|
f0f6f1b71c | ||
|
|
a9d6d5a1db | ||
|
|
7849ce6de7 | ||
|
|
354bdcdbfb | ||
|
|
8ecd3e88c8 | ||
|
|
85d2b627f1 | ||
|
|
88fd92e4dc | ||
|
|
952982e7ea | ||
|
|
22e7c27af7 | ||
|
|
59c18c34de | ||
|
|
6acbaede44 | ||
|
|
1a44f875b3 | ||
|
|
9d935fe1d0 | ||
|
|
e5eed5b9b7 | ||
|
|
05a549f04f | ||
|
|
a5e68a4fae | ||
|
|
016a9e342a | ||
|
|
627d8950ae | ||
|
|
770ce08c10 | ||
|
|
69ac514b3b | ||
|
|
b1481748f9 | ||
|
|
49944e323f | ||
|
|
15310db389 | ||
|
|
ea5c5a2604 | ||
|
|
e461dd1355 | ||
|
|
e299c3e9d8 | ||
|
|
406c47271f | ||
|
|
05c7efea85 | ||
|
|
ce7ddf7055 | ||
|
|
beb40b5db6 | ||
|
|
2def752cc4 | ||
|
|
bacf3ae86a | ||
|
|
0fef382f2e | ||
|
|
95523d8538 | ||
|
|
71f7220bfd | ||
|
|
2212c6deac | ||
|
|
fb3efe4cfd | ||
|
|
e66ac6a7d0 | ||
|
|
7ab3908848 | ||
|
|
921f1fbfe8 | ||
|
|
2ac455f8b5 | ||
|
|
1ce881aed2 | ||
|
|
b1b5140951 | ||
|
|
b109c23233 | ||
|
|
a7a34a0b6e | ||
|
|
4bf63934e1 | ||
|
|
16f572282f | ||
|
|
44380c3700 | ||
|
|
dc46cb1386 | ||
|
|
3ccb1e8ad7 | ||
|
|
d973c5f48b | ||
|
|
f4af78c834 | ||
|
|
e6d323fd30 | ||
|
|
e6ab56aeb5 | ||
|
|
779765b649 | ||
|
|
6da730779a | ||
|
|
a3e77edc57 | ||
|
|
ed00308986 | ||
|
|
89e9092e0f | ||
|
|
f8b11754c8 | ||
|
|
4b38d0793c | ||
|
|
b2156f8154 | ||
|
|
3a5422e635 | ||
|
|
54d3d76868 | ||
|
|
f4dc0ec1b7 | ||
|
|
f500db2dd3 | ||
|
|
95f64f9934 | ||
|
|
cccb0e1a21 | ||
|
|
b434a4227f | ||
|
|
6ba4dc1f04 | ||
|
|
2fe4c81d1e | ||
|
|
5c00264184 | ||
|
|
c744849c9b | ||
|
|
f59b278f00 | ||
|
|
b26c155d5f | ||
|
|
6da79b8745 | ||
|
|
0b92591b17 | ||
|
|
974456db54 | ||
|
|
a1326a80fe | ||
|
|
00d6946b24 | ||
|
|
c4ffde93c0 | ||
|
|
37bfe967ea | ||
|
|
9abbbfd6fb | ||
|
|
155cd08e39 | ||
|
|
e2e6bdf3bd | ||
|
|
810c42c743 | ||
|
|
99e4c1301e | ||
|
|
8c86a831fc | ||
|
|
5e976416a4 | ||
|
|
0339e14260 | ||
|
|
4b94fcebf1 | ||
|
|
ddd2a79f37 | ||
|
|
01a8f2dab3 | ||
|
|
801629d2c1 | ||
|
|
87d62c941f | ||
|
|
7e6e0fdcc5 | ||
|
|
a73b07424c | ||
|
|
0f9b983132 | ||
|
|
43e25902d3 | ||
|
|
2c27c8517f | ||
|
|
b496058a0e | ||
|
|
4313663bd1 | ||
|
|
dbdbfbd07a | ||
|
|
184b23d61f | ||
|
|
5c03b4f668 | ||
|
|
bdbe777d68 | ||
|
|
a838a18647 | ||
|
|
dd8dba63ea | ||
|
|
3f5a664ee7 | ||
|
|
707292e1ff | ||
|
|
9a81b63943 | ||
|
|
30216b7b80 | ||
|
|
b2fc91c2ce | ||
|
|
ef0328833c | ||
|
|
6a93f17a4a | ||
|
|
01bd07ac66 | ||
|
|
88859cfeca | ||
|
|
dfe563e2bc | ||
|
|
7fc0ff981d | ||
|
|
1a9132102d | ||
|
|
d39638282f | ||
|
|
219071654d | ||
|
|
7c415db7bd | ||
|
|
4354c340fc | ||
|
|
a4b484fa36 | ||
|
|
3757d12dc4 | ||
|
|
c3a97b29a9 | ||
|
|
b65e30ec70 | ||
|
|
23a1e0266a | ||
|
|
76acecfe50 | ||
|
|
5031c77afb | ||
|
|
af90b8c989 | ||
|
|
d06b4adad0 | ||
|
|
b961cde946 | ||
|
|
8cbbe2f312 | ||
|
|
c15a49d82d | ||
|
|
fc5b670c73 | ||
|
|
c8adcc4c47 | ||
|
|
41a755fe41 | ||
|
|
8def1bb29e | ||
|
|
d379b04e33 | ||
|
|
17a30e054e | ||
|
|
93809911de | ||
|
|
edeb2ca9f4 | ||
|
|
01662fc3b8 | ||
|
|
7d8f4b4c00 | ||
|
|
e2a3896bf0 | ||
|
|
446de8e0ff | ||
|
|
5ae6c95878 | ||
|
|
7cde349a50 | ||
|
|
61e640f574 | ||
|
|
ed122d9d8e | ||
|
|
34817f4536 | ||
|
|
134d2f0fda | ||
|
|
142973827c | ||
|
|
0998a0eabf | ||
|
|
a96108478b | ||
|
|
47444888c3 | ||
|
|
a4769058f4 | ||
|
|
a4cf4c40d4 | ||
|
|
934fe4d29b | ||
|
|
408012f2e5 | ||
|
|
d0078b830e | ||
|
|
f7f091e18c | ||
|
|
a969430247 | ||
|
|
e52948b2b7 | ||
|
|
53bb1b230b | ||
|
|
54e83aeaaa |
@@ -1,11 +1,24 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [
|
||||
[
|
||||
"cojson",
|
||||
"jazz-tools",
|
||||
"jazz-browser",
|
||||
"jazz-browser-media-images",
|
||||
"jazz-react",
|
||||
"jazz-nodejs",
|
||||
"jazz-run",
|
||||
"cojson-transport-nodejs-ws",
|
||||
"cojson-storage-indexeddb",
|
||||
"cojson-storage-sqlite"
|
||||
]
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
|
||||
147
.github/workflows/build-and-deploy.yaml
vendored
147
.github/workflows/build-and-deploy.yaml
vendored
@@ -11,19 +11,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# example: ["chat", "todo", "pets", "twit", "file-drop"]
|
||||
example: ["twit", "chat"]
|
||||
example: ["chat", "pets", "todo"]
|
||||
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
@@ -35,14 +38,10 @@ jobs:
|
||||
username: gardencmp
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Nuke Workspace
|
||||
- name: Pnpm Build
|
||||
run: |
|
||||
rm package.json yarn.lock;
|
||||
|
||||
- name: Yarn Build
|
||||
run: |
|
||||
yarn install --frozen-lockfile;
|
||||
yarn build;
|
||||
pnpm install
|
||||
pnpm turbo build;
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
- name: Docker Build & Push
|
||||
@@ -54,40 +53,60 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# build-homepage:
|
||||
# runs-on: ubuntu-latest
|
||||
build-homepage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# with:
|
||||
# submodules: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v2
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: gardencmp
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'pnpm'
|
||||
|
||||
# - name: Docker Build & Push
|
||||
# uses: docker/build-push-action@v4
|
||||
# with:
|
||||
# context: ./homepage/homepage-jazz
|
||||
# push: true
|
||||
# tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: gardencmp
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pnpm Install (root)
|
||||
run: |
|
||||
pnpm install
|
||||
working-directory: .
|
||||
|
||||
- name: Pnpm Install & Build (homepage)
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm build;
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- name: Docker Build & Push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./homepage
|
||||
push: true
|
||||
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy-examples:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-examples
|
||||
strategy:
|
||||
matrix:
|
||||
# example: ["chat", "todo", "pets", "twit", "file-drop"]
|
||||
example: ["twit", "chat"]
|
||||
example: ["chat", "pets", "todo"]
|
||||
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -115,38 +134,38 @@ jobs:
|
||||
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
|
||||
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
# deploy-homepage:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: build-homepage
|
||||
deploy-homepage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-homepage
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# with:
|
||||
# submodules: true
|
||||
# - uses: gacts/install-nomad@v1
|
||||
# - name: Tailscale
|
||||
# uses: tailscale/github-action@v1
|
||||
# with:
|
||||
# authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- uses: gacts/install-nomad@v1
|
||||
- name: Tailscale
|
||||
uses: tailscale/github-action@v1
|
||||
with:
|
||||
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
|
||||
|
||||
# - name: Deploy on Nomad
|
||||
# run: |
|
||||
# if [ "${{github.ref_name}}" == "main" ]; then
|
||||
# export BRANCH_SUFFIX="";
|
||||
# export BRANCH_SUBDOMAIN="";
|
||||
# else
|
||||
# export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
|
||||
# export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
|
||||
# fi
|
||||
- name: Deploy on Nomad
|
||||
run: |
|
||||
if [ "${{github.ref_name}}" == "main" ]; then
|
||||
export BRANCH_SUFFIX="";
|
||||
export BRANCH_SUBDOMAIN="";
|
||||
else
|
||||
export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
|
||||
export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
|
||||
fi
|
||||
|
||||
# export DOCKER_USER=gardencmp;
|
||||
# export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||
# export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
|
||||
export DOCKER_USER=gardencmp;
|
||||
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
|
||||
|
||||
# envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
# cat job-instance.nomad;
|
||||
# NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
|
||||
# working-directory: ./homepage/homepage-jazz
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||
working-directory: ./homepage
|
||||
4
.gitignore
vendored
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.
|
||||
@@ -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,476 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 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.51",
|
||||
"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,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).
|
||||
@@ -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,498 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 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.69",
|
||||
"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,446 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 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.68",
|
||||
"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);
|
||||
11
homepage/Dockerfile
Normal file
11
homepage/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:18-slim
|
||||
|
||||
COPY node_modules ./node_modules
|
||||
COPY homepage/.next/standalone ./homepage
|
||||
COPY homepage/.next/static ./homepage/.next/static
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
ENV PORT 3001
|
||||
|
||||
CMD ["node", "homepage/server.js"]
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# If using npm comment out above and use below instead
|
||||
# RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
# set hostname to localhost
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,202 +0,0 @@
|
||||
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
|
||||
import { pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
|
||||
|
||||
export const metadata = {
|
||||
title: "jazz - Global Mesh",
|
||||
description: "Serverless sync & storage for Jazz apps.",
|
||||
};
|
||||
|
||||
# Jazz Global Mesh
|
||||
|
||||
<Slogan>Serverless sync & storage for Jazz apps.</Slogan>
|
||||
|
||||
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
|
||||
Pricing that scales down to zero.
|
||||
|
||||
## The first Collaboration Delivery Network
|
||||
|
||||
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
|
||||
|
||||
<Grid>
|
||||
<GridCard>
|
||||
#### Optimal mesh routing.
|
||||
|
||||
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Smart caching.
|
||||
|
||||
Give users instant load times, with their latest data state always cached close to them.
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Blob storage & media streaming.
|
||||
|
||||
Store files and media streams as idiomatic `CoValues` without S3.
|
||||
</GridCard>
|
||||
</Grid>
|
||||
|
||||
## Pricing
|
||||
|
||||
<Slogan small></Slogan>
|
||||
|
||||
### Free Tier
|
||||
<span className="text-lg font-medium bg-emerald-200 dark:bg-emerald-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</span>
|
||||
<p className="text-sm">Later, any usage under $1/mo will be free.</p>
|
||||
|
||||
|
||||
|
||||
<Grid>
|
||||
<GridItem className="md:col-span-2">
|
||||
### Unlimited <ComingSoonBadge/>
|
||||
|
||||
|
||||
<div className="lg:text-2xl border rounded-lg px-1 py-3 text-center">${pricePer1MtxSyncedOut} <small>per 1M TXs synced out</small> + ${pricePer1MtxStored}<small>/mo per 1M TXs stored</small></div>
|
||||
|
||||
<p><small>$6/mo minimum usage</small></p>
|
||||
|
||||
A TX (transaction) represents an **individual user action**, or **up to 100KB of binary data**.
|
||||
</GridItem>
|
||||
|
||||
<GridItem className="col-start-1">
|
||||
#### Transactions synced out:
|
||||
<div className="text-sm">
|
||||
- Transactions sent out from Global Mesh, each counted once for every device it is synced out to.
|
||||
- Depending on cache behavior each transaction should only be synced out once per connection, ideally once per device requesting it.
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
#### Transactions stored:
|
||||
<div className="text-sm">
|
||||
- Transactions that are continuously persisted.
|
||||
- Counted per second.
|
||||
- Includes backups, hot storage and edge caches.
|
||||
</div>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
**Examples:**
|
||||
|
||||
The number of transactions generated is highly app-specific and depends on user behaviour, but here are some examples:
|
||||
|
||||
<div className="text-sm">
|
||||
- 4 users co-editing 10 pages of text, typing them out as individual character inserts:
|
||||
- 3,000 inserts/page × 10 pages = 30,000 transactions
|
||||
- 30,000 transactions stored = ${30000 * pricePerTxStored} / mo
|
||||
- 3 × 30,000 transactions synced out = ${3 * 30000 * pricePerTxSyncedOut} one-time
|
||||
- 4 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 2h/day for a month
|
||||
- 4 users × 10 FPS × 10s/min × 60min/h * 2h/day × 30days = 1.44M transactions
|
||||
- 1.44M transactions stored = ${1440000 * pricePerTxStored} / mo = ${1440000 * pricePerTxStored / 4} / mo / user
|
||||
- 3 × 1.44M transactions synced out = ${(3 * 1440000 * pricePerTxSyncedOut).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time = ${(3 * 1440000 * pricePerTxSyncedOut / 4).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time / user
|
||||
- A livestreamer streaming video (1GB total) to 100 viewers (combined live & on-demand)
|
||||
- 1GB = 10,000 transactions (100KB each)
|
||||
- 10,000 transactions stored = ${10000 * pricePerTxStored} / mo (= ${10000 * 1000 * pricePerTxStored} per 1TB stored)
|
||||
- 100 × 10,000 transaction synced out = ${100 * 10000 * pricePerTxSyncedOut} one-time (= ${10000 * 1000 * pricePerTxSyncedOut} per 1TB egress)
|
||||
</div>
|
||||
|
||||
## Global Footprint
|
||||
|
||||
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
|
||||
|
||||
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 50ms RTT**
|
||||
- Frankfurt
|
||||
- New York
|
||||
- Newark
|
||||
- North California
|
||||
- North Virginia
|
||||
- San Francisco
|
||||
- Singapore
|
||||
- Toronto
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 100ms RTT**
|
||||
- Amsterdam
|
||||
- Atlanta
|
||||
- London
|
||||
- Ohio
|
||||
- Paris
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 200ms RTT**
|
||||
- Bangalore
|
||||
- Dallas
|
||||
- Mumbai
|
||||
- Oregon
|
||||
|
||||
**Under 300ms RTT**
|
||||
- Seoul
|
||||
- Tokyo
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 400ms RTT**
|
||||
- Sao Paulo
|
||||
- Sydney
|
||||
|
||||
**Under 500ms RTT**
|
||||
- Cape Town
|
||||
</div>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
### Enterprise
|
||||
|
||||
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
|
||||
|
||||
SLAs and dedicated support? White-glove integration services?
|
||||
|
||||
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
|
||||
|
||||
## Custom Deployment Scenarios
|
||||
|
||||
<Slogan>You can rely on Global Mesh. But you don't have to.</Slogan>
|
||||
|
||||
<p>Because Jazz is open-source, you can optionally run your own sync nodes — in a variety of setups.</p>
|
||||
|
||||
<Grid>
|
||||
<GridCard>
|
||||
#### Global Mesh + Data Backup Node.
|
||||
|
||||
<p className="no-prose text-base">Connect your users to Global Mesh for all its benefits, but also run and connect your own data backup node (just in case.)</p>
|
||||
|
||||
<div className="text-sm">
|
||||
Extra costs:
|
||||
- Instance costs for the backup node.
|
||||
- Moderate self-hosted storage costs.
|
||||
- Every transaction is additionally synced to your backup node and counted as synced out.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Global Mesh + DIY Mesh.
|
||||
|
||||
<p className="no-prose text-base">Connect your users to Global Mesh, or your own nodes as a lower-performance fallback. The two networks stay in constant sync.</p>
|
||||
|
||||
<div className="text-sm">
|
||||
Extra costs:
|
||||
- N × instance cost for your sync nodes.
|
||||
- Typically moderate self-hosted egress costs.
|
||||
- High self-hosted storage costs.
|
||||
- Every transaction is additionally synced to your DIY mesh and counted as synced out.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Completely DIY Mesh.
|
||||
|
||||
<p className="no-prose text-base">Build your own network of sync and storage nodes.
|
||||
Handle networking, security and backups yourself.</p>
|
||||
|
||||
<div className="text-sm">
|
||||
Costs:
|
||||
- N × instance cost for your sync nodes.
|
||||
- Very high self-hosted egress costs.
|
||||
- High self-hosted storage costs.
|
||||
</div>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
@@ -1,281 +0,0 @@
|
||||
import {
|
||||
Slogan,
|
||||
Grid,
|
||||
GridItem,
|
||||
GridFeature,
|
||||
GridCard,
|
||||
MultiplayerIcon,
|
||||
ResponsiveIframe,
|
||||
ComingSoonBadge
|
||||
} from "@/components/forMdx";
|
||||
import {
|
||||
JazzLogo
|
||||
} from "@/components/logos";
|
||||
import {
|
||||
WorkflowIcon,
|
||||
UploadCloudIcon,
|
||||
PlaneIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
GaugeIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DataModel_ts,
|
||||
App_tsx,
|
||||
ChatWindow_tsx,
|
||||
} from "@/codeSamples/examples/chat/src";
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
# Instant sync
|
||||
|
||||
<Slogan>Go beyond request/response — ship modern apps with sync.</Slogan>
|
||||
|
||||
Jazz is an open-source toolkit for building apps with **sync** & **secure collaborative data.**
|
||||
|
||||
<h2 className="md:mt-24">Hard things are easy now</h2>
|
||||
|
||||
Jazz replaces APIs, DBs and message queues with **a single new abstraction: CoJSON**.
|
||||
|
||||
This means you get **built-in capabilities** that took best-in-class apps years to build:
|
||||
|
||||
<Grid className="-mt-2 gap-[1px] border rounded-xl overflow-hidden border-stone-200 dark:border-stone-800 shadow-sm bg-stone-200 dark:bg-stone-800 [&>*]:rounded-none [&>*]:border-none [&>*]:bg-stone-50 [&>*]:dark:bg-stone-950">
|
||||
<GridFeature icon={<MonitorSmartphoneIcon />}>Cross-device sync</GridFeature>
|
||||
<GridFeature icon={<MultiplayerIcon/>}>Real-time multiplayer</GridFeature>
|
||||
<GridFeature icon={<WorkflowIcon />}>Automatic granular 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>
|
||||
|
||||
## CoJSON
|
||||
<Slogan small>The collaborative core.</Slogan>
|
||||
|
||||
Jazz is built around **CoJSON,** a new abstraction for **sync** & **secure collaborative data.** And while it does all the heavy lifting...
|
||||
|
||||
- **multi-device co-editing**
|
||||
- **user identities & accounts**
|
||||
- **permissions** & **roles**
|
||||
- **sync** & **caching**
|
||||
- **persistence**
|
||||
|
||||
...its API couldn't be simpler: CoJSON makes collaboration and secure access control feel like **inherent properties of your data**.
|
||||
|
||||
### Collaborative Values
|
||||
|
||||
<Slogan small>Your new building blocks.</Slogan>
|
||||
|
||||
Collaborative Values (CoValues) **can be edited as if they were simple local data,** but they're **automatically encrypted, signed** and **synced** between participants.
|
||||
|
||||
CoValues also **keep their full edit history,** including author metadata and potential editing conflicts. This makes it **super simple to build collaborative and social features.**
|
||||
|
||||
<Grid className="lg:gap-y-8">
|
||||
|
||||
<GridCard>
|
||||
### `CoMap`
|
||||
<div className="text-sm">
|
||||
- Collaborative key-value map
|
||||
- Possible values:
|
||||
- Immutable JSON & IDs of other CoValues
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
### `CoList`
|
||||
<div className="text-sm">
|
||||
- Collaborative ordered list
|
||||
- Possible items:
|
||||
- Immutable JSON & IDs of other CoValues
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
|
||||
The bread and butter of datastructures, with collaboration built-in. You can build whole apps with just these.
|
||||
</GridItem>
|
||||
|
||||
<GridCard>
|
||||
### `CoString` <ComingSoonBadge/>
|
||||
<div className="text-sm">
|
||||
- Collaborative plain-text
|
||||
- Implemented as a CoList of unicode graphemes
|
||||
- Supports concurrent inserts and deletes well
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
### `CoText` <ComingSoonBadge/>
|
||||
<div className="text-sm">
|
||||
- Collaborative rich-text based on `CoString` and a `CoMap` of collaborative markup ranges
|
||||
- Gracefully prevents most editing conflicts
|
||||
- Rendered as markdown, HTML, JSX, etc.
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
|
||||
A shocking amount of UI is text editing. CoJSON offers correct, versatile primitives.
|
||||
</GridItem>
|
||||
<GridCard>
|
||||
|
||||
### `CoStream`
|
||||
<div className="text-sm">
|
||||
- Collection of independent per-user items streams:
|
||||
- Immutable JSON & IDs of other CoValues
|
||||
- Great for presence, reactions, polls, replies etc.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
|
||||
### `BinaryCoStream`
|
||||
<div className="text-sm">
|
||||
- A `CoStream` of binary data chunks
|
||||
- Use for files and media streams
|
||||
- Create, load, sync and store binary blobs or live-streams as just another kind of object
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
|
||||
Two extra tools that let you do everything you need in your app without having to integrate additional external services.
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
### Groups & Accounts
|
||||
|
||||
<Slogan small>First-class user identities & secure permissions.</Slogan>
|
||||
|
||||
<Grid>
|
||||
<GridCard>
|
||||
### `Group`
|
||||
<div className="text-sm">
|
||||
- A scope where specified accounts have roles (`reader`/`writer`/`admin`).
|
||||
- A `Group` owns `CoValues`, with access right determined by group roles.
|
||||
- Accounts can be added to groups directly or using shareable invite secrets.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
### `Account`
|
||||
<div className="text-sm">
|
||||
- Represents a single user and their signing/encryption keys.
|
||||
- Has a private account root and a public profile
|
||||
- Can contain arbitrary app-specific data
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
|
||||
A simple API to define access control from anywhere, verifiably enforced by encryption and signatures.
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
## The Jazz Toolkit
|
||||
|
||||
<Slogan small>Idiomatic bindings for CoJSON, with batteries included.</Slogan>
|
||||
|
||||
Supported environments:
|
||||
<div className="text-sm">
|
||||
- Browser (sync via WebSockets, IndexedDB persistence)
|
||||
- React
|
||||
- Vanilla JS / framework agnostic base
|
||||
- React Native <ComingSoonBadge/>
|
||||
- NodeJS (sync via WebSockets, SQLite persistence) <ComingSoonBadge/>
|
||||
- Swift, Kotlin, Rust <ComingSoonBadge when="later"/>
|
||||
</div>
|
||||
<Grid>
|
||||
|
||||
<GridCard>
|
||||
### Auto-sub
|
||||
<Slogan small>Let your UI drive data-syncing.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Load and auto-subscribe to deeply nested `CoValues` with a reactive hook (or callback).
|
||||
- Access properties & metadata as plain JSON.
|
||||
- Make granular changes with simple mutators.
|
||||
- No queries needed, everything loads on-demand: <br/>
|
||||
`profile?.tweets?.map(tweet => tweet?.text)`
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Cursors & carets
|
||||
<Slogan small>Ready-made spatial presence.</Slogan>
|
||||
<div className="text-sm">
|
||||
- 2D canvas cursors <ComingSoonBadge/>
|
||||
- Text carets <ComingSoonBadge/>
|
||||
- Element-based focus-presence <ComingSoonBadge/>
|
||||
- Scroll-based / out-of-bounds helpers <ComingSoonBadge/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Auth providers
|
||||
|
||||
<Slogan small>Plug and play different kinds of auth.</Slogan>
|
||||
<div className="text-sm">
|
||||
- DemoAuth (for quick multi-user demos)
|
||||
- WebAuthN (TouchID/FaceID)
|
||||
- Auth0, Clerk & Okta <ComingSoonBadge/>
|
||||
- NextAuth <ComingSoonBadge/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Two-way sync to your DB
|
||||
<Slogan small>Add Jazz to an existing app.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Prisma <ComingSoonBadge/>
|
||||
- Drizzle <ComingSoonBadge/>
|
||||
- PostgreSQL introspection <ComingSoonBadge/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### File upload & download
|
||||
|
||||
<Slogan small>Just use `<input type="file"/>`.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Easily convert from and to Browser `Blob`s
|
||||
- Super simple progressive image loading
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Video presence & calls
|
||||
|
||||
<Slogan small>Stream and record audio & video.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Automatic WebRTC connections between `Group` members <ComingSoonBadge/>
|
||||
- Audio/video recording into `BinaryCoStreams` <ComingSoonBadge/>
|
||||
</div>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
|
||||
## Global Mesh
|
||||
|
||||
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
|
||||
|
||||
To give you sync & secure collaborative data instantly on a global scale, we're running Global Mesh. It works with any CoJSON-based app, requires no setup and has straightforward, scale-to-zero pricing.
|
||||
|
||||
Global Mesh is currently free — and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
|
||||
|
||||
<Link href="/mesh" target="_blank">Learn more about Global Mesh</Link>
|
||||
|
||||
## Get Started
|
||||
|
||||
- See the <Link href="https://github.com/gardencmp/jazz#todo-list" target="_blank">Todo List Example Walkthrough</Link>
|
||||
- <Link href="https://github.com/gardencmp/jazz/blob/main/DOCS.md" target="_blank">Read the docs</Link>
|
||||
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">Join our Discord</Link>
|
||||
File diff suppressed because one or more lines are too long
@@ -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,85 +0,0 @@
|
||||
import { ComingSoonBadge, Grid, GridCard, GridItem } from "./forMdx";
|
||||
|
||||
export const pricePer1MtxSyncedOut = 1;
|
||||
export const pricePer1MtxStored = 2;
|
||||
|
||||
export const pricePerTxSyncedOut = pricePer1MtxSyncedOut / 1_000_000;
|
||||
export const pricePerTxStored = pricePer1MtxStored / 1_000_000;
|
||||
|
||||
export function Pricing() {
|
||||
|
||||
const worstCaseBytesPerTx = 200_000;
|
||||
const avgCaseBytesPerTx = 10_000;
|
||||
|
||||
const worstCaseCostPerTBstorage = 20;
|
||||
const worstCaseCostPerTxStored =
|
||||
worstCaseBytesPerTx * (worstCaseCostPerTBstorage / 1_000_000_000_000);
|
||||
const avgCaseCostPerTxStored =
|
||||
avgCaseBytesPerTx * (worstCaseCostPerTBstorage / 1_000_000_000_000);
|
||||
|
||||
const costPerTBEgress = 5;
|
||||
const serverCost = 30;
|
||||
const txOutPerSecondPerServer = 100;
|
||||
const txPerMonthPerServer = txOutPerSecondPerServer * 60 * 60 * 24 * 30;
|
||||
const worstCaseCostPerTxSyncedOut =
|
||||
worstCaseBytesPerTx * (costPerTBEgress / 1_000_000_000_000) +
|
||||
serverCost / txPerMonthPerServer;
|
||||
const avgCaseCostPerTxSyncedOut =
|
||||
avgCaseBytesPerTx * (costPerTBEgress / 1_000_000_000_000) +
|
||||
serverCost / txPerMonthPerServer;
|
||||
|
||||
const recommendedSyncToStorageRatio = 0.2;
|
||||
|
||||
const freeTierSyncedOut = 100_000;
|
||||
const freeTierStored = freeTierSyncedOut / recommendedSyncToStorageRatio;
|
||||
|
||||
const proTierSyncedOut = 500_000;
|
||||
const proTierStored = proTierSyncedOut / recommendedSyncToStorageRatio;
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<GridCard>
|
||||
<h3>Free Tier</h3>
|
||||
<p className="text-lg font-medium bg-indigo-200 dark:bg-indigo-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</p>
|
||||
<p className="text-sm">Later, any usage under $2/mo will be free.</p>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
<h3>Unlimited <ComingSoonBadge/></h3>
|
||||
<p className="text-lg">
|
||||
{fmt$(pricePer1MtxSyncedOut)} per 1,000,000 transactions
|
||||
synced out
|
||||
{/* <br />
|
||||
Avg cost: {fmt$(avgCaseCostPerTxSyncedOut * 1_000_000)}
|
||||
<br />
|
||||
Worst cost: {fmt$(worstCaseCostPerTxSyncedOut * 1_000_000)} */}
|
||||
<br/>
|
||||
{fmt$(pricePer1MtxStored)}
|
||||
<small>/mo</small> per 1,000,000 transactions stored
|
||||
{/* <br />
|
||||
Avg cost: {fmt$(avgCaseCostPerTxStored * 1_000_000)}
|
||||
<br />
|
||||
Worst cost: {fmt$(worstCaseCostPerTxStored * 1_000_000)} */}
|
||||
</p>
|
||||
<p className="text-sm">See below for how transactions are defined.</p>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
<h3>Enterprise</h3>
|
||||
<p className="text-sm">Custom deployment in the cloud, your private cloud, on-premises or hybrids?</p>
|
||||
<p className="text-sm">SLAs and dedicated support? White-glove integration services?</p>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function fmt(num: number) {
|
||||
return num.toLocaleString("en-US", {});
|
||||
}
|
||||
|
||||
function fmt$(num: number) {
|
||||
return (
|
||||
"$" +
|
||||
num.toLocaleString("en-US", {
|
||||
maximumSignificantDigits: 3,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { MDXComponents } from 'mdx/types'
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...components,
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import createMDX from "@next/mdx";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Configure `pageExtensions`` to include MDX files
|
||||
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
|
||||
// Optionally, add any other Next.js config below
|
||||
experimental: {
|
||||
serverActions: true,
|
||||
},
|
||||
};
|
||||
|
||||
const withMDX = createMDX({
|
||||
// Add markdown plugins here, as desired
|
||||
options: {
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
},
|
||||
});
|
||||
|
||||
const config = {
|
||||
...withMDX(nextConfig),
|
||||
output: 'standalone'
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "homepage-jazz",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@evilmartians/harmony": "^1.0.0",
|
||||
"@icons-pack/react-simple-icons": "^9.1.0",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "^13.5.4",
|
||||
"@types/mdx": "^2.0.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.284.0",
|
||||
"next": "13.5.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"shiki-twoslash": "^3.1.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@csstools/postcss-oklab-function": "^3.0.6",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@csstools/postcss-oklab-function': { 'preserve': true },
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
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!
|
||||
|
||||
426
homepage/homepage/app/docs/guide.mdx
Normal file
426
homepage/homepage/app/docs/guide.mdx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { Slogan } from "@/components/forMdx";
|
||||
import { JazzLogo } from "@/components/logos";
|
||||
|
||||
<h1 id="guide">Learn some Jazz.</h1>
|
||||
<Slogan>Build an issue tracking app with distributed state.</Slogan>
|
||||
|
||||
Our issues app will be quite simple, but it will have team collaboration. <span className="text-nowrap">**Let's call it... “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="setup">Project Setup</h2>
|
||||
|
||||
1. Create a project from a generic Vite starter template:
|
||||
|
||||
```bash
|
||||
npx degit gardencmp/vite-ts-react-tailwind circular
|
||||
cd circular
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You should now have an empty app running, typically at [localhost:5173](http://localhost:5173).<br/>
|
||||
|
||||
<small>
|
||||
(If you make changes to the code, the app will automatically refresh.)
|
||||
</small>
|
||||
|
||||
2. Install `jazz-tools` and `jazz-react`<br/>
|
||||
|
||||
<small>(in a new terminal window):</small>
|
||||
|
||||
```bash
|
||||
cd circular
|
||||
npm install jazz-tools jazz-react
|
||||
```
|
||||
|
||||
3. Set up a Jazz context, by modifying `src/main.tsx`:
|
||||
|
||||
```tsx subtle=1,2,3,4,13,15,16,17,19
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { JazzReact } from "jazz-react";
|
||||
|
||||
const Jazz = createJazzReactContext({
|
||||
auth: DemoAuth({ appName: "Circular" }),
|
||||
peer: "wss://mesh.jazz.tools/?key=you@example.com", // <- put your email here to receive a proper API key for later
|
||||
});
|
||||
export const { useAccount, useCoState } = Jazz;
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<Jazz.Provider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</Jazz.Provider>,
|
||||
);
|
||||
```
|
||||
|
||||
This sets Jazz up, extracts app-specific hooks for later, and wraps our app in the provider.
|
||||
|
||||
TODO: explain Auth
|
||||
|
||||
<h2 id="intro-to-covalues">Intro to CoValues</h2>
|
||||
|
||||
Let's learn about the **central idea** behind Jazz: **Collaborative Values.**
|
||||
|
||||
What if we could **treat distributed state like local state?** That's what CoValues do.
|
||||
|
||||
We can
|
||||
|
||||
- **create** CoValues, anywhere
|
||||
- **load** CoValues by `ID`, from anywhere else
|
||||
- **edit** CoValues, from anywhere, by mutating them like local state
|
||||
- **subscribe to edits** in CoValues, whether they're local or remote
|
||||
|
||||
<h3 id="first-schema">Declaring our own CoValues</h3>
|
||||
|
||||
To make our own CoValues, we first need to declare a schema for them. Think of a schema as a combination of TypeScript types and runtime type information.
|
||||
|
||||
Let's start by defining a schema for our most central entity in Circular: an **Issue.**
|
||||
|
||||
Create a new file `src/schema.ts` and add the following:
|
||||
|
||||
```ts
|
||||
import { CoMap, co } from "jazz-tools";
|
||||
|
||||
export class Issue extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
estimate = co.number;
|
||||
status? = co.literal("backlog", "in progress", "done");
|
||||
}
|
||||
```
|
||||
|
||||
TODO: explain what's happening
|
||||
|
||||
<h3>Reading from CoValues</h3>
|
||||
|
||||
CoValues are designed to be read like simple local JSON state. Let's see how we can read from an Issue by building a component to render one.
|
||||
|
||||
Create a new file `src/components/Issue.tsx` and add the following:
|
||||
|
||||
```tsx
|
||||
import { Issue } from "../schema";
|
||||
|
||||
export function IssueComponent({ issue }, { issue: Issue }) {
|
||||
return (
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
|
||||
<h2>{issue.title}</h2>
|
||||
<p className="col-span-3">{issue.description}</p>
|
||||
<p>Estimate: {issue.estimate}</p>
|
||||
<p>Status: {issue.status}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Simple enough!
|
||||
|
||||
<h3>Creating CoValues</h3>
|
||||
|
||||
To actually see an Issue, we have to create one. This is where things start to get interesting...
|
||||
|
||||
Let's modify `src/App.tsx` to prepare for creating an Issue and then rendering it:
|
||||
|
||||
```tsx subtle=5,13,14,15
|
||||
import { useState, useCallback } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue";
|
||||
|
||||
function App() {
|
||||
const [issue, setIssue] = useState<Issue>();
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
Now, finally, let's implement creating an issue:
|
||||
|
||||
```tsx subtle=1,2,3,5,6,8,23,24,25,27,28,29,30
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue";
|
||||
import { useAccount } from "./main";
|
||||
|
||||
function App() {
|
||||
const { me } = useAccount();
|
||||
const [issue, setIssue] = useState<Issue>();
|
||||
|
||||
const createIssue = () => {
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
description: "Make sure it's big enough for 10 snails.",
|
||||
estimate: 5,
|
||||
status: "backlog",
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
setIssue(newIssue);
|
||||
};
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
Now you should be able to create a new issue by clicking the button and then see it rendered.
|
||||
|
||||
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider">
|
||||
Preview
|
||||
</div>
|
||||
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
|
||||
<h2>Buy terrarium</h2>
|
||||
<p className="col-span-3">Make sure it's big enough for 10 snails.</p>
|
||||
<p>Estimate: 5</p>
|
||||
<p>Status: backlog</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
We'll already notice one interesting thing here:
|
||||
|
||||
- We have to create every CoValue with an `owner`!
|
||||
- this will determine access rights on the CoValue, which we'll learn about in "Groups & Permissions"
|
||||
- here we set `owner` to the current user `me`, which we get from the Jazz context / `useAccount`
|
||||
|
||||
**Behind the scenes, Jazz not only creates the Issue in memory but also automatically syncs an encrypted version to the cloud and persists it locally. The Issue also has a globally unique ID.**
|
||||
|
||||
We'll make use of both of these facts in a bit, but for now let's start with local editing and subscribing.
|
||||
|
||||
<h3>Editing CoValues and subscribing to edits</h3>
|
||||
|
||||
Since we're the owner of the CoValue, we should be able to edit it, right?
|
||||
|
||||
And since this is a React app, it would be nice to subscribe to edits of the CoValue and reactively re-render the UI, like we can with local state.
|
||||
|
||||
This is exactly what the `useCoState` hook is for!
|
||||
|
||||
- Note that `useCoState` doesn't take a CoValue directly, but rather a CoValue's schema, plus its `ID`.
|
||||
- So we'll slightly adapt our `useState` to only keep track of an issue ID...
|
||||
- ...and then use `useCoState` to get the actual issue
|
||||
|
||||
Let's modify `src/App.tsx`:
|
||||
|
||||
```tsx subtle=1,2,3,4,5,6,7,12,13,14,15,16,17,18,19,20,21,23,25,26,27,28,29,30,32
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue";
|
||||
import { useAccount } from "./main";
|
||||
|
||||
function App() {
|
||||
const { me } = useAccount();
|
||||
const [issueID, setIssueID] = useState<ID<Issue>>();
|
||||
|
||||
const issue = useCoState(Issue, issueID);
|
||||
|
||||
const createIssue = () => {
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
description: "Make sure it's big enough for 10 snails.",
|
||||
estimate: 5,
|
||||
status: "backlog",
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
setIssueID(newIssue.id);
|
||||
};
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
And now for the exciting part! Let's make `src/components/Issue.tsx` an editing component.
|
||||
|
||||
```tsx subtle=1,3,4,5,28,29,30,31
|
||||
import { Issue } from "../schema";
|
||||
|
||||
export function IssueComponent({ issue }, { issue: Issue }) {
|
||||
return (
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
|
||||
<input type="text"
|
||||
value={issue.title}
|
||||
onChange={(event) => { issue.title = event.target.value }}/>
|
||||
<textarea className="col-span-3"
|
||||
value={issue.description}
|
||||
onChange={(event) => { issue.description = event.target.value }}/>
|
||||
<label className="flex">
|
||||
Estimate:
|
||||
<input type="number" className="text-right min-w-0"
|
||||
value={issue.estimate}
|
||||
onChange={(event) => { issue.estimate = Number(event.target.value) }}/>
|
||||
</label>
|
||||
<select
|
||||
value={issue.status}
|
||||
onChange={(event) => {
|
||||
issue.status = event.target.value as "backlog" | "in progress" | "done"
|
||||
}}
|
||||
>
|
||||
<option value="backlog">Backlog</option>
|
||||
<option value="in progress">In Progress</option>
|
||||
<option value="done">Done</options>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider">
|
||||
Preview
|
||||
</div>
|
||||
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
|
||||
<input type="text" value={"Buy terrarium"} />
|
||||
<input
|
||||
type="text"
|
||||
className="col-span-3"
|
||||
value={"Make sure it's big enough for 10 snails."}
|
||||
/>
|
||||
<label className="flex">
|
||||
Estimate:{" "}
|
||||
<input type="number" value={5} className="text-right min-w-0" />
|
||||
</label>
|
||||
<select value={"backlog"}>
|
||||
<option value="backlog">Backlog</option>
|
||||
<option value="in progress">In Progress</option>
|
||||
<option value="done">Done</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
You'll immediately notice that we're doing something non-idiomatic for React: we mutate the issue directly, by assigning to its properties.
|
||||
|
||||
This works because CoValues intercept these edits, update their local view accordingly (React doesn't really care after rendering) and then notify subscribers of the change, who will receive a fresh, updated view of the CoValue.
|
||||
|
||||
<aside className="text-sm border 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, { as: me }, (updatedIssue) => {
|
||||
console.log(updatedIssue);
|
||||
});
|
||||
```
|
||||
|
||||
3. If you're in a React component, to re-render reactively:
|
||||
`tsx
|
||||
const issue = useCoState(Issue, issueID);
|
||||
`
|
||||
|
||||
By the way, `useCoState` is basically just an optimized version of
|
||||
|
||||
```ts
|
||||
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
|
||||
const { me } = useAccount();
|
||||
const [value, setValue] = useState<V>();
|
||||
|
||||
useEffect(() => Schema.subscribe(id, { as: me }, setValue), [id]);
|
||||
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
</aside>
|
||||
|
||||
We have one subscriber on our Issue, with `useCoState` in `src/App.tsx`, which will cause the `App` component and its children **to** re-render whenever the Issue changes.
|
||||
|
||||
<h3>Automatic local & cloud persistence</h3>
|
||||
|
||||
So far our Issue CoValues just looked like ephemeral local state. We'll now start exploring the first main feature that makes CoValues special: **automatic persistence.**
|
||||
|
||||
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 URL state and make sure our useState is in sync with that.
|
||||
|
||||
```tsx subtle=1,2,3,4,5,6,7,12,13,14,15,16,17,18,19,20,21,22,23,24,26,27,28,29,30,31,32,33,34,35
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue";
|
||||
import { useAccount } from "./main";
|
||||
|
||||
function App() {
|
||||
const { me } = useAccount();
|
||||
const [issueID, setIssueID] = useState<ID<Issue>>(
|
||||
window.location.search?.replace("?issue=", "") || undefined
|
||||
);
|
||||
|
||||
const issue = useCoState(Issue, issueID);
|
||||
|
||||
const createIssue = () => {
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
description: "Make sure it's big enough for 10 snails.",
|
||||
estimate: 5,
|
||||
status: "backlog",
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
setIssueID(newIssue.id);
|
||||
window.history.pushState({}, "", `?issue=${newIssue.id}`);
|
||||
};
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
Now you should be able to create an issue, reload the page, and still see the same issue.
|
||||
|
||||
|
||||
<h3>Remote sync</h3>
|
||||
|
||||
But even better, you should be able to:
|
||||
|
||||
- copy the URL to a new tab and see the same issue
|
||||
- edit the issue and see the changes reflected in the other tab
|
||||
|
||||
|
||||
<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="refs-and-on-demand-subscribe">Refs & Auto-Subscribe</h2>
|
||||
|
||||
<h2 id="groups-and-permissions">Groups & Permissions</h2>
|
||||
|
||||
<h2 id="accounts-and-migrations">Accounts & Migrations</h2>
|
||||
|
||||
<h2 id="backend-workers">Backend Workers</h2>
|
||||
55
homepage/homepage/app/docs/page.tsx
Normal file
55
homepage/homepage/app/docs/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
import { PackageDocs } from "@/components/docs/packageDocs";
|
||||
import Guide from "./guide.mdx";
|
||||
|
||||
export const metadata = {
|
||||
title: "jazz - Docs",
|
||||
description: "Jazz Guide, FAQ & Docs.",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<div className="hidden md:block bg-stone-100 dark:bg-stone-900 p-4 rounded-xl sticky overflow-y-scroll overscroll-contain w-[16rem] h-[calc(100dvh-8rem)] -mb-[calc(100dvh-8rem)] top-[6rem] mr-10 prose-sm prose-ul:pl-1 prose-ul:ml-1 prose-li:my-2 prose-li:leading-tight prose-ul:list-['-']">
|
||||
<DocNav />
|
||||
</div>
|
||||
|
||||
<div className="md:ml-[20rem]">
|
||||
<Guide />
|
||||
|
||||
<h1 id="faq">FAQ</h1>
|
||||
|
||||
<p>
|
||||
<span className="text-amber-500">
|
||||
🚧 OH NO - We don't have any FAQ yet. 🚧
|
||||
</span>{" "}
|
||||
{"->"}{" "}
|
||||
<a href="https://github.com/gardencmp/jazz/issues/187">
|
||||
Complain on GitHub
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div className="xl:-mr-[calc(50vw-40rem)]">
|
||||
<h1>API Reference</h1>
|
||||
|
||||
<p>
|
||||
<span className="text-amber-500">
|
||||
🚧 OH NO - These docs are still highly
|
||||
work-in-progress. 🚧
|
||||
</span>{" "}
|
||||
{"->"}{" "}
|
||||
<a href="https://github.com/gardencmp/jazz/issues/188">
|
||||
Complain on GitHub
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<PackageDocs package="jazz-tools" />
|
||||
<PackageDocs package="jazz-react" />
|
||||
<PackageDocs package="jazz-browser" />
|
||||
<PackageDocs package="jazz-browser-media-images" />
|
||||
<PackageDocs package="jazz-nodejs" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -99,7 +99,7 @@ pre.shiki div.highlight {
|
||||
background-color: #f1f8ff;
|
||||
}
|
||||
pre.shiki div.line {
|
||||
min-height: 1rem;
|
||||
/* min-height: 1rem; */
|
||||
counter-increment: lineNumber 1;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,12 @@ pre.shiki div.line::before {
|
||||
width: 1.3rem;
|
||||
padding-right: 0.3rem;
|
||||
text-align: right;
|
||||
@apply text-stone-200 dark:text-stone-800 text-[0.65rem];
|
||||
transition: color 0.3s;
|
||||
@apply text-stone-200/50 dark:text-stone-900 text-[0.65rem];
|
||||
}
|
||||
|
||||
pre.shiki div.line:hover::before {
|
||||
@apply text-stone-400 dark:text-stone-600;
|
||||
}
|
||||
|
||||
/** Don't show the language identifiers */
|
||||
@@ -147,7 +152,7 @@ pre.twoslash data-lsp:hover::before {
|
||||
}
|
||||
|
||||
.shiki-outer {
|
||||
@apply shadow-sm rounded-xl border border-stone-200 dark:border-stone-800;
|
||||
@apply shadow-sm rounded-xl;
|
||||
}
|
||||
|
||||
.shiki-filename {
|
||||
@@ -156,7 +161,7 @@ pre.twoslash data-lsp:hover::before {
|
||||
|
||||
pre .code-container {
|
||||
overflow: scroll;
|
||||
@apply p-2 pl-0 bg-stone-50 dark:bg-stone-950 rounded-b-xl text-[0.8rem] leading-4;
|
||||
@apply p-2 pl-0 bg-stone-75 dark:bg-stone-925 rounded-b-xl text-[0.8rem] leading-tight border border-stone-100 dark:border-stone-900;
|
||||
}
|
||||
/* The try button */
|
||||
pre .code-container > a {
|
||||
@@ -324,7 +329,12 @@ data-lsp {
|
||||
}
|
||||
.tag-container .twoslash-annotation {
|
||||
position: absolute;
|
||||
font-family: "JetBrains Mono", Menlo, Monaco, Consolas, Courier New,
|
||||
font-family:
|
||||
"JetBrains Mono",
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
Courier New,
|
||||
monospace;
|
||||
right: -10px;
|
||||
/** Default annotation text to 200px */
|
||||
@@ -10,10 +10,19 @@ import { GcmpLogo, JazzLogo } from "@/components/logos";
|
||||
import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
|
||||
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
|
||||
import { MailIcon } from "lucide-react";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
|
||||
// If loading a variable font, you don't need to specify the font weight
|
||||
const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope" });
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-manrope",
|
||||
display: "swap",
|
||||
});
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
});
|
||||
const pragmata = localFont({
|
||||
src: "../fonts/ppr_0829.woff2",
|
||||
variable: "--font-ppr",
|
||||
@@ -46,14 +55,13 @@ export default function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Nav
|
||||
mainLogo={<JazzLogo className="w-24" />}
|
||||
mainLogo={<JazzLogo className="w-24 -ml-2" />}
|
||||
items={[
|
||||
{ title: "Toolkit", href: "/" },
|
||||
{ title: "Global Mesh", href: "/mesh" },
|
||||
{ title: "Home", href: "/" },
|
||||
{ title: "Sync & Storage Mesh", href: "/mesh" },
|
||||
{
|
||||
title: "Docs & Guides",
|
||||
href: "https://github.com/gardencmp/jazz/blob/main/DOCS.md",
|
||||
newTab: true,
|
||||
title: "Docs",
|
||||
href: "/docs",
|
||||
},
|
||||
{
|
||||
title: "Blog",
|
||||
@@ -90,11 +98,11 @@ export default function RootLayout({
|
||||
icon: <SiTwitter className="w-5" />,
|
||||
},
|
||||
]}
|
||||
docNav={<DocNav />}
|
||||
/>
|
||||
<main className="flex min-h-screen flex-col p-8 max-w-[80rem] w-full">
|
||||
<main className="flex min-h-screen flex-col p-8 max-w-[80rem] w-full [&_*]:scroll-mt-[6rem]">
|
||||
<article
|
||||
className={[
|
||||
"pt-20",
|
||||
"prose lg:prose-lg max-w-none prose-stone dark:prose-invert",
|
||||
"prose-headings:font-display",
|
||||
"prose-h1:text-5xl lg:prose-h1:text-6xl prose-h1:font-medium prose-h1:tracking-tighter",
|
||||
@@ -102,12 +110,15 @@ export default function RootLayout({
|
||||
"prose-p:max-w-3xl prose-p:leading-snug",
|
||||
"prose-strong:font-medium",
|
||||
"prose-code:font-normal prose-code:leading-tight prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:-my-1 prose-code:rounded",
|
||||
"prose-pre:max-w-3xl prose-pre:text-[0.8em] prose-pre:leading-[1.3] prose-pre:-mt-4 prose-pre:my-4 prose-pre:px-3 prose-pre:py-2 md:prose-pre:-mx-3 prose-pre:bg-stone-100 dark:prose-pre:bg-stone-900",
|
||||
|
||||
"prose-inner-code:font-normal prose-inner-code:text-[1em]",
|
||||
].join(" ")}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
</main>
|
||||
<footer className="flex mt-10 min-h-[15rem] -mb-20 bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
|
||||
<footer className="flex z-10 mt-10 min-h-[15rem] -mb-20 bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
|
||||
<div className="p-8 max-w-[80rem] w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
|
||||
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
|
||||
<GcmpLogo monochrome className="w-32" />
|
||||
@@ -129,14 +140,13 @@ export default function RootLayout({
|
||||
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
|
||||
href="/mesh"
|
||||
>
|
||||
Global Mesh
|
||||
Sync & Storage Mesh
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
|
||||
href="https://github.com/gardencmp/jazz/blob/main/DOCS.md"
|
||||
newTab
|
||||
href="/docs"
|
||||
>
|
||||
Docs & Guides
|
||||
Docs
|
||||
</NavLink>
|
||||
</div>
|
||||
{/* <div className="flex flex-col gap-2 text-sm">
|
||||
@@ -192,14 +202,17 @@ export default function RootLayout({
|
||||
</div>
|
||||
<div className="col-span-3 md:col-start-2 lg:col-start-auto flex flex-col gap-2 text-sm">
|
||||
Sign up for updates:
|
||||
|
||||
|
||||
<Newsletter/>
|
||||
<Newsletter />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</ThemeProvider>
|
||||
<script defer data-api="/api/event" data-domain="jazz.tools" src="/js/script.js"></script>
|
||||
<script
|
||||
defer
|
||||
data-api="/api/event"
|
||||
data-domain="jazz.tools"
|
||||
src="/js/script.js"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
253
homepage/homepage/app/mesh/page.mdx
Normal file
253
homepage/homepage/app/mesh/page.mdx
Normal file
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
Slogan,
|
||||
Grid,
|
||||
GridCard,
|
||||
GridItem,
|
||||
ComingSoonBadge,
|
||||
} from "@/components/forMdx";
|
||||
|
||||
export const metadata = {
|
||||
title: "jazz - Jazz Mesh",
|
||||
description: "Serverless sync & storage for Jazz apps.",
|
||||
};
|
||||
|
||||
<div className="md:pt-20" />
|
||||
|
||||
# Sync & Storage Mesh
|
||||
|
||||
<Slogan>The first Collaboration Delivery Network.</Slogan>
|
||||
|
||||
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
|
||||
Pricing that scales down to zero.
|
||||
|
||||
<Grid>
|
||||
<GridCard>
|
||||
#### Optimal mesh routing.
|
||||
|
||||
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
|
||||
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Smart caching.
|
||||
|
||||
Give users instant load times, with their latest data state always cached close to them.
|
||||
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Blob storage & media streaming.
|
||||
|
||||
Store files and media streams as idiomatic `CoValues` without S3.
|
||||
|
||||
</GridCard>
|
||||
</Grid>
|
||||
|
||||
## Pricing
|
||||
|
||||
<Slogan small></Slogan>
|
||||
|
||||
<Grid>
|
||||
<GridCard>
|
||||
### Mesh Free
|
||||
<span className="text-2xl">$0</span>
|
||||
|
||||
- Best-effort sync
|
||||
- 3,000 sync-minutes/mo
|
||||
- 1 GB storage
|
||||
{/* - Egress/mo: 5 million ops <span className="text-xs">or 50GB blobs</span> */}
|
||||
{/* - Storage: 2.5 million ops <span className="text-xs">or 25GB blobs</span> */}
|
||||
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
|
||||
### Mesh Starter <ComingSoonBadge/>
|
||||
|
||||
<span className="text-2xl">$9</span>/mo
|
||||
|
||||
- Base-priority sync
|
||||
- 6,000 sync-minutes/mo
|
||||
- 100 GB storage
|
||||
|
||||
<div className="text-xs">
|
||||
Extra usage:
|
||||
- $9 per additional 6,000 sync-minutes
|
||||
- $9 per additional 1TB storage/mo
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
### Mesh Pro <ComingSoonBadge/>
|
||||
<span className="text-2xl">$79</span>/mo
|
||||
|
||||
- High-priority sync
|
||||
- 30,000 sync-minutes/mo
|
||||
- 1 TB storage
|
||||
- Offer sync.yourdomain.com
|
||||
|
||||
<div className="text-xs">
|
||||
Extra usage:
|
||||
- $15 per additional 6,000 sync-minutes
|
||||
- $15 per additional 1TB storage/mo
|
||||
</div>
|
||||
</GridCard>
|
||||
{/*<GridCard>
|
||||
### Mesh Enterprise <ComingSoonBadge/>
|
||||
|
||||
<span className="text-2xl">Custom</span>
|
||||
- Custom SLA
|
||||
- Custom cloud deployment
|
||||
- Dedicated support
|
||||
- Audit logs
|
||||
</GridCard>*/}
|
||||
</Grid>
|
||||
|
||||
### FAQ:
|
||||
|
||||
<div className="text-sm">
|
||||
#### How are sync-minutes counted?
|
||||
|
||||
Sync-minutes are counted on a **per-connected-device, per-minute basis.**<br/>
|
||||
A device is considered syncing **only when it's actively sending or receiving data.**
|
||||
|
||||
#### How can I estimate my usage?
|
||||
|
||||
The best way to estimate your usage is to **guess how many minutes per month each user will spend actively using your app.**
|
||||
Storage is mostly determined by large binary blobs (like images or videos) that you store in Jazz.
|
||||
|
||||
#### What happens if I exceed my plan's limits?
|
||||
|
||||
All limits are initially soft limits, so don't worry if you suddenly get lots of users or traffic!
|
||||
|
||||
Sync beyond the limit is still served, but at a lower priority.
|
||||
Data beyond the storage limit is still stored and backed up, but may be significantly slower to access.
|
||||
If you exceed your plan's limits consistently, we'll reach out to discuss upgrading your plan.
|
||||
|
||||
</div>
|
||||
|
||||
{/* *### Examples:* */}
|
||||
|
||||
<div className="text-sm">
|
||||
|
||||
</div>
|
||||
|
||||
## Global Footprint
|
||||
|
||||
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
|
||||
|
||||
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 50ms RTT**
|
||||
- Frankfurt
|
||||
- New York
|
||||
- Newark
|
||||
- North California
|
||||
- North Virginia
|
||||
- San Francisco
|
||||
- Singapore
|
||||
- Toronto
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 100ms RTT**
|
||||
- Amsterdam
|
||||
- Atlanta
|
||||
- London
|
||||
- Ohio
|
||||
- Paris
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 200ms RTT**
|
||||
- Bangalore
|
||||
- Dallas
|
||||
- Mumbai
|
||||
- Oregon
|
||||
|
||||
**Under 300ms RTT**
|
||||
|
||||
- Seoul
|
||||
- Tokyo
|
||||
|
||||
</div>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<div className="text-sm">
|
||||
**Under 400ms RTT**
|
||||
- Sao Paulo
|
||||
- Sydney
|
||||
|
||||
**Under 500ms RTT**
|
||||
|
||||
- Cape Town
|
||||
|
||||
</div>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
### Enterprise
|
||||
|
||||
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
|
||||
|
||||
SLAs and dedicated support? White-glove integration services?
|
||||
|
||||
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
|
||||
|
||||
## Custom Deployment Scenarios
|
||||
|
||||
<Slogan small>You can rely on Jazz Mesh. But you don't have to.</Slogan>
|
||||
|
||||
<p>
|
||||
Because Jazz is open-source, you can optionally run your own sync nodes
|
||||
— in a variety of setups.
|
||||
</p>
|
||||
|
||||
<Grid>
|
||||
<GridCard>
|
||||
#### Jazz Mesh + Data Backup Node.
|
||||
|
||||
<p className="no-prose text-base">
|
||||
Connect your users to Jazz Mesh for all its benefits, but also run and
|
||||
connect your own data backup node (just in case.)
|
||||
</p>
|
||||
|
||||
<div className="text-sm">
|
||||
Extra costs:
|
||||
- Instance costs for the backup node.
|
||||
- Moderate self-hosted storage costs.
|
||||
- Your backup node is counted as a continously connected device.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Jazz Mesh + DIY Mesh.
|
||||
|
||||
<p className="no-prose text-base">
|
||||
Connect your users to Jazz Mesh, or your own nodes as a lower-performance
|
||||
fallback. The two networks stay in constant sync.
|
||||
</p>
|
||||
|
||||
<div className="text-sm">
|
||||
Extra costs:
|
||||
- N × instance cost for your sync nodes.
|
||||
- Typically moderate self-hosted egress costs.
|
||||
- High self-hosted storage costs.
|
||||
- Any of your sync nodes connected to Jazz Mesh is counted as a continously connected device.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### Completely DIY Mesh.
|
||||
|
||||
<p className="no-prose text-base">
|
||||
Build your own network of sync and storage nodes. Handle networking,
|
||||
security and backups yourself.
|
||||
</p>
|
||||
|
||||
<div className="text-sm">
|
||||
Costs:
|
||||
- N × instance cost for your sync nodes.
|
||||
- Very high self-hosted egress costs.
|
||||
- High self-hosted storage costs.
|
||||
</div>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
278
homepage/homepage/app/page.mdx
Normal file
278
homepage/homepage/app/page.mdx
Normal file
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
Slogan,
|
||||
Grid,
|
||||
GridItem,
|
||||
GridFeature,
|
||||
GridCard,
|
||||
MultiplayerIcon,
|
||||
ResponsiveIframe,
|
||||
ComingSoonBadge,
|
||||
} from "@/components/forMdx";
|
||||
import { JazzLogo, LocalFirstConfLogo } from "@/components/logos";
|
||||
import {
|
||||
WorkflowIcon,
|
||||
UploadCloudIcon,
|
||||
PlaneIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
GaugeIcon,
|
||||
UsersIcon,
|
||||
FileLock2Icon,
|
||||
HardDriveDownloadIcon,
|
||||
KeyRoundIcon
|
||||
} from "lucide-react";
|
||||
import { App_tsx, ChatScreen_tsx } from "@/codeSamples/examples/chat/src";
|
||||
import Link from "next/link";
|
||||
|
||||
<div className="md:pt-20" />
|
||||
|
||||
<a href="https://app.localfirstconf.com/schedule/conference/every-app-secretly-wants-to-be-local-first" className="-mt-8 md:-mt-20 float-right top-[5rem] right-4 border border-stone-700 dark:border-stone-300 rounded flex gap-3 items-center px-4 py-2 mb-4 rotate-2 md:rotate-6 no-underline hover:scale-105 transition-transform">
|
||||
<div className="text-sm font-bold uppercase">See you in Berlin<br/>May 30-31!</div>
|
||||
<LocalFirstConfLogo className="w-24"/>
|
||||
</a>
|
||||
|
||||
# Instant sync.
|
||||
|
||||
<Slogan>A new way to build apps with distributed state.</Slogan>
|
||||
|
||||
<Grid className="gap-[1px] -mx-4 md:-mx-6 rounded-xl overflow-hidden bg-stone-50 dark:bg-stone-950 [&>*]:rounded-none [&>*]:border-none [&>*]:bg-stone-100 [&>*]:dark:bg-stone-900">
|
||||
<GridFeature icon={<MonitorSmartphoneIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Cross-device sync</GridFeature>
|
||||
<GridFeature icon={<MultiplayerIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Real-time multiplayer</GridFeature>
|
||||
<GridFeature icon={<UsersIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Team/social features</GridFeature>
|
||||
<GridFeature icon={<FileLock2Icon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Built-in permissions</GridFeature>
|
||||
<GridFeature icon={<UploadCloudIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Cloud sync & storage</GridFeature>
|
||||
<GridFeature icon={<HardDriveDownloadIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>On-device storage</GridFeature>
|
||||
<GridFeature icon={<GaugeIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Instant UI updates</GridFeature>
|
||||
<GridFeature icon={<KeyRoundIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>E2EE & signatures</GridFeature>
|
||||
|
||||
<div className="col-start-1 row-start-1 row-span-2 col-span-2 px-4 md:px-6 text-base">
|
||||
<h2 className="not-prose text-2xl font-medium tracking-tight mt-3 md:mt-5 mb-6">Hard things are easy now.</h2>
|
||||
|
||||
Jazz is an **open-source toolkit** that replaces APIs, databases and message queues with **a single new abstraction:**
|
||||
|
||||
**“Collaborative Values”** — **distributed state** with **secure permissions built-in.**
|
||||
|
||||
Features that used to take months to build now work out-of-the-box.
|
||||
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<div className="-mx-[calc(min(0,(100vw-95rem)/2))]">
|
||||
### First impressions…
|
||||
<Slogan small>A chat app in 84 lines of code.</Slogan>
|
||||
|
||||
<Grid className="mt-0 -mx-4 md:-mx-6">
|
||||
<GridItem className="md:col-start-1 col-span-2">
|
||||
|
||||
<App_tsx/>
|
||||
|
||||
</GridItem>
|
||||
<GridItem className="md:col-start-3 col-span-2">
|
||||
|
||||
<ChatScreen_tsx/>
|
||||
|
||||
</GridItem>
|
||||
<ResponsiveIframe src="https://chat.jazz.tools" localSrc="http://localhost:5173" className="lg:col-start-5 col-span-2 rounded-xl overflow-hidden min-h-[50vh]"/>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
## Collaborative Values
|
||||
|
||||
<Slogan small>Your new building blocks.</Slogan>
|
||||
|
||||
<div className='text-base'>
|
||||
Based on CRDTs and public-key cryptography, CoValues...
|
||||
|
||||
- Can be read & edited like simple local JSON state
|
||||
- Can be created anywhere, are automatically synced & persisted
|
||||
- Always keep full edit history & author metadata
|
||||
- Automatically resolve most conflicts
|
||||
|
||||
</div>
|
||||
|
||||
### Bread-and-butter datastructures
|
||||
|
||||
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
|
||||
|
||||
<GridCard>
|
||||
#### `CoMap`
|
||||
<div className="text-sm">
|
||||
- Collaborative key-value map - Possible values: - Immutable JSON &
|
||||
other CoValues
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
#### `CoList`
|
||||
<div className="text-sm">
|
||||
- Collaborative ordered list - Possible items: - Immutable JSON & other
|
||||
CoValues
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
#### `CoPlainText` & `CoRichText` <ComingSoonBadge />
|
||||
<div className="text-sm">
|
||||
- Collaborative plain-text & rich-text - Gracefully prevents most
|
||||
editing conflicts - Rendered as markdown, HTML, JSX, etc.
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
#### `CoStream`
|
||||
<div className="text-sm">
|
||||
- Collection of independent per-user items streams:
|
||||
- Immutable JSON & other CoValues
|
||||
- Great for presence, reactions, polls, replies etc.
|
||||
</div>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
|
||||
### First-class files & binary data
|
||||
|
||||
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
|
||||
|
||||
<GridCard>
|
||||
#### `BinaryCoStream`
|
||||
<div className="text-sm">
|
||||
- Represents a file or live binary stream - Can be referenced and synced
|
||||
like any other CoValue - Can easily be converted from/to browser `Blob`s
|
||||
- <code>{`<input type="file"/>`}</code> -> `BinaryCoStream` -> `Blob` ->
|
||||
`BlobURL`
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
#### `ImageDefinition`
|
||||
<div className="text-sm">
|
||||
- Represents multiple resolutions of the same image - Can be
|
||||
progressively loaded, including super fast blur preview & image size
|
||||
info
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
</Grid>
|
||||
|
||||
### Secure permissions, authorship & teams
|
||||
|
||||
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
|
||||
|
||||
<GridCard>
|
||||
#### `Group`
|
||||
<div className="text-sm">
|
||||
- A scope where specified accounts have roles (`reader`/`writer`/`admin`).
|
||||
- A `Group` owns `CoValues`, with access right determined by group roles.
|
||||
- Accounts can be added to groups directly or using shareable invite secrets.
|
||||
</div>
|
||||
</GridCard>
|
||||
<GridCard>
|
||||
#### `Account`
|
||||
<div className="text-sm">
|
||||
- Represents a single user and their signing/encryption keys.
|
||||
- Has a private account root and a public profile
|
||||
- Can contain arbitrary app-specific data
|
||||
</div>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
|
||||
## The Jazz Toolkit
|
||||
|
||||
<Slogan small>A high-level toolkit for building apps around CoValues.</Slogan>
|
||||
|
||||
Supported environments:
|
||||
|
||||
<div className="text-sm">
|
||||
- Browser (sync via WebSockets, IndexedDB persistence)
|
||||
- React
|
||||
- Vanilla JS / framework agnostic base
|
||||
- React Native <ComingSoonBadge/>
|
||||
- NodeJS (sync via WebSockets, SQLite persistence) <ComingSoonBadge/>
|
||||
- Swift, Kotlin, Rust <ComingSoonBadge when="later"/>
|
||||
</div>
|
||||
<Grid>
|
||||
|
||||
<GridCard>
|
||||
### Auto-sub
|
||||
<Slogan small>Let your UI drive data-syncing.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Load and auto-subscribe to deeply nested `CoValues` with a reactive
|
||||
hook (or callback). - Access properties & metadata as plain JSON. - Make
|
||||
granular changes with simple mutators. - No queries needed, everything
|
||||
loads on-demand: <br />
|
||||
`profile?.tweets?.map(tweet => tweet?.text)`
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Cursors & carets
|
||||
<Slogan small>Ready-made spatial presence.</Slogan>
|
||||
<div className="text-sm">
|
||||
- 2D canvas cursors <ComingSoonBadge />
|
||||
- Text carets <ComingSoonBadge />
|
||||
- Element-based focus-presence <ComingSoonBadge />
|
||||
- Scroll-based / out-of-bounds helpers <ComingSoonBadge />
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Auth Providers
|
||||
|
||||
<Slogan small>Plug and play different kinds of auth.</Slogan>
|
||||
<div className="text-sm">
|
||||
- DemoAuth (for quick multi-user demos)
|
||||
- WebAuthN (TouchID/FaceID)
|
||||
- Auth0, Clerk & Okta <ComingSoonBadge/>
|
||||
- NextAuth <ComingSoonBadge/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Two-way sync to your DB
|
||||
<Slogan small>Add Jazz to an existing app.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Prisma <ComingSoonBadge />
|
||||
- Drizzle <ComingSoonBadge />
|
||||
- PostgreSQL introspection <ComingSoonBadge />
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### File upload & download
|
||||
|
||||
<Slogan small>Just use `<input type="file"/>`.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Easily convert from and to Browser `Blob`s
|
||||
- Super simple progressive image loading
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
<GridCard>
|
||||
### Video presence & calls
|
||||
|
||||
<Slogan small>Stream and record audio & video.</Slogan>
|
||||
<div className="text-sm">
|
||||
- Automatic WebRTC connections between `Group` members <ComingSoonBadge/>
|
||||
- Audio/video recording into `BinaryCoStreams` <ComingSoonBadge/>
|
||||
</div>
|
||||
</GridCard>
|
||||
</Grid>
|
||||
|
||||
## Jazz Mesh
|
||||
|
||||
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
|
||||
|
||||
To give you sync and secure collaborative data instantly on a global scale, we're running Jazz Mesh. It works with any Jazz-based app, requires no setup and has straightforward, scale-to-zero pricing.
|
||||
|
||||
Jazz Mesh is currently free — and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
|
||||
|
||||
<Link href="/mesh" target="_blank">
|
||||
Learn more about Jazz Mesh
|
||||
</Link>
|
||||
|
||||
## Get Started
|
||||
|
||||
- <Link href="/docs" target="_blank">
|
||||
Read the docs
|
||||
</Link>
|
||||
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">
|
||||
Join our Discord
|
||||
</Link>
|
||||
16
homepage/homepage/components.json
Normal file
16
homepage/homepage/components.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,18 @@
|
||||
import { useLayoutEffect, useState, useRef, IframeHTMLAttributes } from "react";
|
||||
|
||||
export function ResponsiveIframe(
|
||||
props: IframeHTMLAttributes<HTMLIFrameElement>
|
||||
props: IframeHTMLAttributes<HTMLIFrameElement> & { localSrc: string },
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [url, setUrl] = useState<string | undefined>(props.src);
|
||||
const [url, setUrl] = useState<string | undefined>();
|
||||
const [src, setSrc] = useState<string | undefined>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const listener = (e: MessageEvent) => {
|
||||
console.log(e);
|
||||
if (e.data.type === "navigate" && props.src?.startsWith(e.origin)) {
|
||||
if (e.data.type === "navigate" && src?.startsWith(e.origin)) {
|
||||
setUrl(e.data.url);
|
||||
}
|
||||
};
|
||||
@@ -21,7 +22,7 @@ export function ResponsiveIframe(
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, [props.src]);
|
||||
}, [src]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
@@ -38,10 +39,23 @@ export function ResponsiveIframe(
|
||||
};
|
||||
}, [containerRef]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setSrc(
|
||||
window.location.hostname === "localhost"
|
||||
? props.localSrc
|
||||
: props.src,
|
||||
);
|
||||
setUrl(
|
||||
window.location.hostname === "localhost"
|
||||
? props.localSrc
|
||||
: props.src,
|
||||
);
|
||||
}, [props.src, props.localSrc]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-full h-full flex flex-col items-stretch border border-stone-200 dark:border-stone-800 " +
|
||||
"w-full h-full flex flex-col items-stretch border border-stone-100 dark:border-stone-900 " +
|
||||
props.className
|
||||
}
|
||||
>
|
||||
@@ -57,6 +71,7 @@ export function ResponsiveIframe(
|
||||
<div className="flex-grow" ref={containerRef}>
|
||||
<iframe
|
||||
{...props}
|
||||
src={src}
|
||||
className="dark:bg-black"
|
||||
{...dimensions}
|
||||
allowFullScreen
|
||||
17
homepage/homepage/components/breadcrumb.tsx
Normal file
17
homepage/homepage/components/breadcrumb.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function BreadCrumb({
|
||||
items,
|
||||
}: {
|
||||
items: { title: string; href: string }[];
|
||||
}) {
|
||||
const pathName = usePathname();
|
||||
|
||||
return (
|
||||
<span className="text-sm font-bold">
|
||||
{items.find((item) => item.href === pathName)?.title}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
158
homepage/homepage/components/docs/nav.tsx
Normal file
158
homepage/homepage/components/docs/nav.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ClassRef, PropRef } from "./tags";
|
||||
import { requestProject } from "./requestProject";
|
||||
import { PackageIcon } from "lucide-react";
|
||||
|
||||
export function DocNav() {
|
||||
return (
|
||||
<>
|
||||
<p className="mt-0 not-prose font-medium">
|
||||
<DocNavLink href="#guide">Guide</DocNavLink>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<DocNavLink href="#guide-setup">Project Setup</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Intro to CoValues
|
||||
</DocNavLink>
|
||||
<ul>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Declaration
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Reading
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Creation
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Editing & Subscription
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Persistence
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Remote Sync
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#intro-to-covalues">
|
||||
Public Sharing
|
||||
</DocNavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#refs-and-on-demand-subscribe">
|
||||
Refs & Auto-Subscribe
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#groups-and-permissions">
|
||||
Groups & Permissions
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#accounts-and-migrations">
|
||||
Accounts & Migrations
|
||||
</DocNavLink>
|
||||
</li>
|
||||
<li>
|
||||
<DocNavLink href="#backend-workers">
|
||||
Backend Workers
|
||||
</DocNavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="font-medium border-t -mx-4 px-4 pt-4 border-stone-200 dark:border-stone-800">
|
||||
<DocNavLink href="#faq">FAQ</DocNavLink>
|
||||
</p>
|
||||
|
||||
<NavPackage package="jazz-tools" />
|
||||
<NavPackage package="jazz-react" />
|
||||
<NavPackage package="jazz-browser" />
|
||||
<NavPackage package="jazz-browser-media-images" />
|
||||
<NavPackage package="jazz-nodejs" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function NavPackage({
|
||||
package: packageName,
|
||||
}: {
|
||||
package: string;
|
||||
}) {
|
||||
let project = await requestProject(packageName as any);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-sm not-prose mt-4 flex gap-1 items-center -mx-4 px-4 pt-4 border-t border-stone-200 dark:border-stone-800 ">
|
||||
<code className="font-bold">{packageName}</code>{" "}
|
||||
<PackageIcon size={15} strokeWidth={1.5} />
|
||||
</h2>
|
||||
{project.categories?.map((category) => {
|
||||
return (
|
||||
<details
|
||||
key={category.title}
|
||||
open={category.title !== "Other"}
|
||||
className="[&:not([open])_summary]:after:content-['...']"
|
||||
>
|
||||
<summary className="block text-xs mt-2 cursor-pointer">
|
||||
{category.title}
|
||||
</summary>
|
||||
<div className="text-sm -ml-0.5 max-w-full text-balance">
|
||||
{category.children.map(
|
||||
(child, i, children) =>
|
||||
(i == 0 ||
|
||||
child.name !==
|
||||
children[i - 1]!.name) && (
|
||||
<>
|
||||
<a
|
||||
key={child.id}
|
||||
className="inline-block not-prose px-1 m-0.5 bg-stone-200 dark:bg-stone-800 rounded opacity-70 hover:opacity-100 cursor-pointer"
|
||||
href={`#${packageName}/${child.name}`}
|
||||
>
|
||||
<code>{child.name}</code>
|
||||
</a>
|
||||
{"\u200B"}
|
||||
</>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocNavLink({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="not-prose hover:text-black dark:hover:text-white"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
438
homepage/homepage/components/docs/packageDocs.tsx
Normal file
438
homepage/homepage/components/docs/packageDocs.tsx
Normal file
@@ -0,0 +1,438 @@
|
||||
import {
|
||||
CommentDisplayPart,
|
||||
DeclarationReflection,
|
||||
ReflectionKind,
|
||||
SignatureReflection,
|
||||
SomeType,
|
||||
TypeContext,
|
||||
TypeParameterReflection,
|
||||
} from "typedoc";
|
||||
import {
|
||||
ClassOrInterface,
|
||||
DocComment,
|
||||
FnDecl,
|
||||
Highlight,
|
||||
PropCategory,
|
||||
PropDecl,
|
||||
} from "./tags";
|
||||
import { requestProject } from "./requestProject";
|
||||
import { PackageIcon, Type } from "lucide-react";
|
||||
|
||||
export async function PackageDocs({
|
||||
package: packageName,
|
||||
}: {
|
||||
package: string;
|
||||
}) {
|
||||
let project = await requestProject(packageName as any);
|
||||
|
||||
// console.dir(project, {depth: 10});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="flex items-center gap-2">
|
||||
<code>{packageName}</code> <PackageIcon />
|
||||
</h2>
|
||||
{project.categories?.map((category) => {
|
||||
return (
|
||||
<section key={category.title}>
|
||||
<h3>{category.title}</h3>
|
||||
{category.children.map((child) => (
|
||||
<RenderPackageChild
|
||||
child={child}
|
||||
key={child.id}
|
||||
inPackage={packageName}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderPackageChild({
|
||||
child,
|
||||
inPackage,
|
||||
}: {
|
||||
child: DeclarationReflection;
|
||||
inPackage: string;
|
||||
}) {
|
||||
if (
|
||||
child.kind === ReflectionKind.Class ||
|
||||
child.kind === ReflectionKind.Interface
|
||||
) {
|
||||
return (
|
||||
<RenderClassOrInterface
|
||||
classOrInterface={child}
|
||||
inPackage={inPackage}
|
||||
/>
|
||||
);
|
||||
} else if (child.kind === ReflectionKind.TypeAlias) {
|
||||
return <RenderTypeAlias inPackage={inPackage} child={child} />;
|
||||
} else if (child.kind === ReflectionKind.Function) {
|
||||
return child.getAllSignatures().map((signature, i) => {
|
||||
const paramTypes = printParamsWithTypes(signature);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
id={inPackage + "/" + child.name}
|
||||
className="not-prose mt-4"
|
||||
>
|
||||
{
|
||||
<Highlight hide={[0, 2]}>
|
||||
{`function \n${printSimpleSignature(child, signature) + ":"}\n {}`}
|
||||
</Highlight>
|
||||
}{" "}
|
||||
<span className="opacity-75 text-xs pl-1">
|
||||
<Highlight>{printType(signature.type)}</Highlight>
|
||||
</span>
|
||||
<div className="ml-4 mt-0 text-xs opacity-75 flex">
|
||||
{paramTypes.length > 0 && (
|
||||
<div>
|
||||
<Highlight
|
||||
hide={[0, 1 + paramTypes.length]}
|
||||
>{`function fn(...args: [\n${paramTypes.join(
|
||||
",\n",
|
||||
)}\n]) {}`}</Highlight>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return (
|
||||
<h4 id={inPackage + "/" + child.name}>
|
||||
{child.name} {child.type?.type}
|
||||
</h4>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RenderTypeAlias({
|
||||
inPackage,
|
||||
child,
|
||||
}: {
|
||||
inPackage: string;
|
||||
child: DeclarationReflection;
|
||||
}) {
|
||||
const typeParameters = child.typeParameters?.map(
|
||||
(tParam) =>
|
||||
tParam.name +
|
||||
(tParam.type ? ` extends ${printType(tParam.type)}` : ""),
|
||||
);
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h4 className="not-prose" id={inPackage + "/" + child.name}>
|
||||
<Highlight>{`type ${child.name}`}</Highlight>
|
||||
</h4>
|
||||
<p className="not-prose text-sm ml-4">
|
||||
<Highlight>{`type ${child.name}${typeParameters?.length && `<${typeParameters?.join(", ")}>`} = ${printType(
|
||||
child.type,
|
||||
)}`}</Highlight>
|
||||
</p>
|
||||
<div className="ml-4 mt-2 flex-[3]">
|
||||
<DocComment>
|
||||
{child.comment
|
||||
? renderSummary(child.comment.summary)
|
||||
: "⚠️ undocumented"}
|
||||
</DocComment>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderClassOrInterface({
|
||||
inPackage,
|
||||
classOrInterface: classOrInterface,
|
||||
}: {
|
||||
inPackage: string;
|
||||
classOrInterface: DeclarationReflection;
|
||||
}) {
|
||||
const commentSummary = classOrInterface.comment?.summary;
|
||||
const typeParamsWithConstraints = printTypeParamsWithConstraints(
|
||||
classOrInterface.typeParameters,
|
||||
);
|
||||
return (
|
||||
<ClassOrInterface
|
||||
inPackage={inPackage}
|
||||
name={classOrInterface.name}
|
||||
doc={renderSummary(commentSummary)}
|
||||
isInterface={classOrInterface.kind === ReflectionKind.Interface}
|
||||
typeParameters={
|
||||
classOrInterface.typeParameters?.length
|
||||
? "<" +
|
||||
classOrInterface.typeParameters
|
||||
.map((tParam) => tParam.name)
|
||||
.join(", ") +
|
||||
">"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{typeParamsWithConstraints.length > 0 && (
|
||||
<div className="text-sm -mt-4">
|
||||
<Highlight
|
||||
hide={[0, 1 + typeParamsWithConstraints.length]}
|
||||
>{`class Thing<\n${typeParamsWithConstraints.join(
|
||||
",\n",
|
||||
)}\n]> {}`}</Highlight>
|
||||
</div>
|
||||
)}
|
||||
{classOrInterface.categories?.map((category) => (
|
||||
<div key={category.title}>
|
||||
<PropCategory
|
||||
name={category.title}
|
||||
description={renderSummary(
|
||||
category.description?.filter(
|
||||
(p) =>
|
||||
p.kind !== "code" ||
|
||||
!p.text.startsWith("```"),
|
||||
),
|
||||
)}
|
||||
example={renderSummary(
|
||||
category.description?.filter(
|
||||
(p) =>
|
||||
p.kind === "code" &&
|
||||
p.text.startsWith("```"),
|
||||
),
|
||||
)}
|
||||
/>
|
||||
{category.children.map((prop) => (
|
||||
<RenderProp
|
||||
prop={prop}
|
||||
klass={classOrInterface}
|
||||
key={prop.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</ClassOrInterface>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSummary(commentSummary: CommentDisplayPart[] | undefined) {
|
||||
return commentSummary?.map((part, idx) =>
|
||||
part.kind === "text" ? (
|
||||
<span key={idx}>{part.text.split("\n").map((line, i, lines) => <>
|
||||
{line}
|
||||
{i !== lines.length - 1 && <br />}
|
||||
</>)}</span>
|
||||
) : part.kind === "inline-tag" ? (
|
||||
<code key={idx}>
|
||||
{part.tag} {part.text}
|
||||
</code>
|
||||
) : part.text.startsWith("```") ? (
|
||||
<pre key={idx} className="text-xs mt-4">
|
||||
<code>
|
||||
<Highlight>
|
||||
{part.text.split("\n").slice(1, -1).join("\n")}
|
||||
</Highlight>
|
||||
</code>
|
||||
</pre>
|
||||
) : (
|
||||
<code key={idx}>
|
||||
<Highlight>{part.text.slice(1, -1)}</Highlight>
|
||||
</code>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function RenderProp({
|
||||
prop,
|
||||
klass,
|
||||
}: {
|
||||
prop: DeclarationReflection;
|
||||
klass: DeclarationReflection;
|
||||
}) {
|
||||
const propOrGetSig = prop.getSignature ? prop.getSignature : prop;
|
||||
return prop.kind & ReflectionKind.FunctionOrMethod ? (
|
||||
prop
|
||||
.getAllSignatures()
|
||||
.map((signature) => (
|
||||
<FnDecl
|
||||
key={signature.id}
|
||||
signature={printSimplePropSignature(prop, klass, signature)}
|
||||
typeParams={printTypeParamsWithConstraints(
|
||||
signature.typeParameters,
|
||||
)}
|
||||
paramTypes={printParamsWithTypes(signature)}
|
||||
returnType={printType(signature.type)}
|
||||
doc={renderSummary(signature.comment?.summary)}
|
||||
example={renderSummary(
|
||||
signature.comment?.getTag("@example")?.content,
|
||||
)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<PropDecl
|
||||
name={
|
||||
(prop.flags.isStatic ? klass.name : "") +
|
||||
(prop.name.startsWith("[") ? "" : ".") +
|
||||
prop.name
|
||||
}
|
||||
type={printType(propOrGetSig.type)}
|
||||
doc={
|
||||
propOrGetSig.comment &&
|
||||
renderSummary(propOrGetSig.comment.summary)
|
||||
}
|
||||
example={renderSummary(
|
||||
propOrGetSig.comment?.getTag("@example")?.content,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function printSimplePropSignature(
|
||||
prop: DeclarationReflection,
|
||||
klass: DeclarationReflection,
|
||||
signature: SignatureReflection,
|
||||
): string {
|
||||
return (
|
||||
`${prop.flags.isStatic ? klass.name : ""}.` +
|
||||
printSimpleSignature(prop, signature)
|
||||
);
|
||||
}
|
||||
|
||||
function printSimpleSignature(
|
||||
item: DeclarationReflection,
|
||||
signature: SignatureReflection,
|
||||
) {
|
||||
return `${item.name}${
|
||||
signature.typeParameters?.length
|
||||
? "<" +
|
||||
signature.typeParameters.map((tParam) => tParam.name).join(", ") +
|
||||
">"
|
||||
: ""
|
||||
}(${printParams(signature)?.join(", ")})`;
|
||||
}
|
||||
|
||||
function printParams(signature: SignatureReflection) {
|
||||
return (
|
||||
signature.parameters?.flatMap((param) =>
|
||||
param.name === "this"
|
||||
? []
|
||||
: [
|
||||
param.name === "__namedParameters" &&
|
||||
param.type?.type === "reflection"
|
||||
? "{ " +
|
||||
param.type.declaration.children
|
||||
?.map(
|
||||
(child) =>
|
||||
child.name +
|
||||
(child.flags.isOptional ? "?" : ""),
|
||||
)
|
||||
.join(", ") +
|
||||
" }"
|
||||
: param.name + (param.defaultValue ? "?" : ""),
|
||||
],
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
function printParamsWithTypes(signature: SignatureReflection) {
|
||||
return (
|
||||
signature.parameters?.map(
|
||||
(param) =>
|
||||
(param.name === "__namedParameters"
|
||||
? ""
|
||||
: param.name + (param.defaultValue ? "?" : "") + ": ") +
|
||||
printType(param.type),
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
function printTypeParamsWithConstraints(
|
||||
typeParams: TypeParameterReflection[] | undefined,
|
||||
): string[] {
|
||||
return (
|
||||
typeParams?.flatMap((tParam) =>
|
||||
tParam.type
|
||||
? [`${tParam.name} extends ${printType(tParam.type)}`]
|
||||
: [],
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
function printType(type: SomeType | undefined): string {
|
||||
if (!type) return "NO TYPE";
|
||||
if (type.type === "reflection") {
|
||||
if (type.declaration.kind === ReflectionKind.TypeLiteral) {
|
||||
if (type.declaration.signatures?.length) {
|
||||
return (
|
||||
type.declaration.signatures
|
||||
?.map(
|
||||
(sig) =>
|
||||
`(${printParamsWithTypes(sig).join(
|
||||
", ",
|
||||
)}) => ${printType(sig.type)}`,
|
||||
)
|
||||
.join(" | ") || ""
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
"{ " +
|
||||
type.declaration.children
|
||||
?.map(
|
||||
(child) =>
|
||||
`${child.name}: ${printType(child.type)}`,
|
||||
)
|
||||
.join(", ") +
|
||||
" }"
|
||||
);
|
||||
}
|
||||
}
|
||||
return "TODO reflection type " + type.declaration.kind;
|
||||
} else if (type.type === "reference") {
|
||||
return (
|
||||
type.name +
|
||||
(type.typeArguments?.length
|
||||
? "<" + type.typeArguments.map(printType).join(", ") + ">"
|
||||
: "")
|
||||
);
|
||||
} else if (type.type === "intersection") {
|
||||
return (
|
||||
type.types
|
||||
?.map((part) =>
|
||||
part.needsParenthesis(TypeContext["intersectionElement"])
|
||||
? `(${printType(part)})`
|
||||
: printType(part),
|
||||
)
|
||||
.join(" & ") || "NO TYPES"
|
||||
);
|
||||
} else if (type.type === "union") {
|
||||
return (
|
||||
type.types
|
||||
.sort((a, b) => (a.type === "intrinsic" ? 1 : -1))
|
||||
?.map((part) =>
|
||||
part.needsParenthesis(TypeContext["unionElement"])
|
||||
? `(${printType(part)})`
|
||||
: printType(part),
|
||||
)
|
||||
.join(" | ") || "NO TYPES"
|
||||
);
|
||||
} else if (type.type === "tuple") {
|
||||
return `[${type.elements.map(printType).join(", ")}]`;
|
||||
} else if (type.type === "array") {
|
||||
if (type.needsParenthesis()) {
|
||||
return `(${printType(type.elementType)})[]`;
|
||||
} else {
|
||||
return printType(type.elementType) + "[]";
|
||||
}
|
||||
} else if (type.type === "mapped") {
|
||||
return `{[${type.parameter} in ${printType(
|
||||
type.parameterType,
|
||||
)}]: ${printType(type.templateType)}}`;
|
||||
} else if (type.type === "indexedAccess") {
|
||||
return `${printType(type.objectType)}[${printType(type.indexType)}]`;
|
||||
} else if (type.type === "intrinsic") {
|
||||
return type.name;
|
||||
} else if (type.type === "predicate") {
|
||||
return `${type.name} is ${printType(type.targetType)}`;
|
||||
} else if (type.type === "query") {
|
||||
return printType(type.queryType);
|
||||
} else if (type.type === "literal") {
|
||||
return JSON.stringify(type.value);
|
||||
} else {
|
||||
return "TODO type " + type.type;
|
||||
}
|
||||
}
|
||||
24
homepage/homepage/components/docs/requestProject.ts
Normal file
24
homepage/homepage/components/docs/requestProject.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { ProjectReflection, Deserializer, JSONOutput } from "typedoc";
|
||||
|
||||
import JazzToolsDocs from "../../typedoc/jazz-tools.json";
|
||||
import JazzReactDocs from "../../typedoc/jazz-react.json";
|
||||
import JazzBrowserDocs from "../../typedoc/jazz-browser.json";
|
||||
import JazzBrowserMediaImagesDocs from "../../typedoc/jazz-browser-media-images.json";
|
||||
import JazzNodejsDocs from "../../typedoc/jazz-nodejs.json";
|
||||
|
||||
const docs = {
|
||||
"jazz-tools": JazzToolsDocs as JSONOutput.ProjectReflection,
|
||||
"jazz-react": JazzReactDocs as JSONOutput.ProjectReflection,
|
||||
"jazz-browser": JazzBrowserDocs as JSONOutput.ProjectReflection,
|
||||
"jazz-browser-media-images":
|
||||
JazzBrowserMediaImagesDocs as JSONOutput.ProjectReflection,
|
||||
"jazz-nodejs": JazzNodejsDocs as JSONOutput.ProjectReflection,
|
||||
};
|
||||
|
||||
export async function requestProject(
|
||||
packageName: keyof typeof docs,
|
||||
): Promise<ProjectReflection> {
|
||||
const deserializer = new Deserializer({} as any);
|
||||
return deserializer.reviveProject(docs[packageName], packageName);
|
||||
}
|
||||
240
homepage/homepage/components/docs/tags.tsx
Normal file
240
homepage/homepage/components/docs/tags.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import { ReactNode } from "react";
|
||||
import { getHighlighter } from "shiki";
|
||||
|
||||
const highlighter = getHighlighter({
|
||||
langs: ["typescript", "bash"],
|
||||
theme: "css-variables", // use the theme
|
||||
});
|
||||
|
||||
export async function Highlight({
|
||||
children,
|
||||
hide,
|
||||
lang = "typescript",
|
||||
}: {
|
||||
children: string;
|
||||
hide?: number[];
|
||||
lang?: string;
|
||||
}) {
|
||||
const lines = (await highlighter).codeToThemedTokens(
|
||||
children,
|
||||
lang,
|
||||
"css-variables",
|
||||
);
|
||||
|
||||
return (
|
||||
<code>
|
||||
{lines
|
||||
.filter((_, i) => !hide?.includes(i))
|
||||
.map((line, i, all) => (
|
||||
<>
|
||||
{line.map((token, i) => (
|
||||
<span key={i} style={{ color: token.color }}>
|
||||
{token.content}
|
||||
</span>
|
||||
))}
|
||||
{i !== all.length - 1 && <br />}
|
||||
</>
|
||||
))}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClassOrInterface({
|
||||
inPackage,
|
||||
name,
|
||||
typeParameters,
|
||||
children,
|
||||
doc,
|
||||
isInterface,
|
||||
}: {
|
||||
inPackage: string;
|
||||
name: string;
|
||||
typeParameters?: string;
|
||||
children: ReactNode;
|
||||
doc: ReactNode;
|
||||
isInterface?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative not-prose">
|
||||
<a
|
||||
id={inPackage + "/" + name}
|
||||
href={"#" + inPackage + "/" + name}
|
||||
className="absolute md:top-[3.5rem] -ml-6 w-4 flex items-center justify-center opacity-0 peer-group-hover:opacity-100 target:opacity-100"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<LinkIcon size={15} />
|
||||
</a>
|
||||
<h4 className="peer sticky top-0 pt-[0.5rem] md:top-[2.5rem] md:pt-[3rem] bg-stone-50 dark:bg-stone-950 z-20">
|
||||
<a href={"#" + inPackage + "/" + name}>
|
||||
<Highlight>
|
||||
{(isInterface ? "interface " : "class ") +
|
||||
name +
|
||||
typeParameters}
|
||||
</Highlight>
|
||||
</a>
|
||||
</h4>
|
||||
<div className="pl-2">
|
||||
<div className=" mt-4 text-sm">{doc}</div>
|
||||
<div className="">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClassRef({ name }: { name: string }) {
|
||||
return <Highlight hide={[0]}>{`class\n${name}`}</Highlight>;
|
||||
}
|
||||
|
||||
export function PropRef({ on, prop }: { on?: string; prop: string }) {
|
||||
return on ? (
|
||||
<Highlight>{`${on}.${prop}`}</Highlight>
|
||||
) : (
|
||||
<Highlight>{prop}</Highlight>
|
||||
);
|
||||
}
|
||||
|
||||
export function PropDecl({
|
||||
name,
|
||||
type,
|
||||
doc,
|
||||
example,
|
||||
}: {
|
||||
name?: string;
|
||||
type?: string;
|
||||
doc: ReactNode;
|
||||
example?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="py-2 border-t border-stone-200 dark:border-stone-800 mt-4 text-sm">
|
||||
<div>
|
||||
{name && <Highlight>{name + ":"}</Highlight>}
|
||||
{" "}
|
||||
{type && (
|
||||
<span className="opacity-75 text-xs pl-1">
|
||||
<Highlight
|
||||
hide={[0, 1, 2 + type.split("\n").length]}
|
||||
>{`class X {\nprop:\n${type}`}</Highlight>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="lg:flex gap-2">
|
||||
<div className="ml-4 mt-2 flex-[3]">
|
||||
<DocComment>{doc || "⚠️ undocumented"}</DocComment>
|
||||
</div>
|
||||
{example && (
|
||||
<div className="ml-4 lg:ml-0 lg:mt-0 flex-[1] relative w-full overflow-x-scroll col-span-2 pl-4 md:pl-0 md:mt-0 text-xs opacity-60 group-hover:opacity-100">
|
||||
<div className="opacity-30 text-xs -mb-4">Example:</div>
|
||||
{example}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FnDecl({
|
||||
signature,
|
||||
typeParams,
|
||||
paramTypes,
|
||||
returnType,
|
||||
doc,
|
||||
example,
|
||||
}: {
|
||||
signature: string;
|
||||
typeParams: string[];
|
||||
paramTypes: string[];
|
||||
returnType: string;
|
||||
doc: ReactNode;
|
||||
example: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="py-2 border-t border-stone-200 dark:border-stone-800 mt-4 text-sm">
|
||||
<div>
|
||||
{<Highlight>{signature + ":"}</Highlight>}{" "}
|
||||
<span className="opacity-75 text-xs pl-1">
|
||||
<Highlight>{returnType}</Highlight>
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-4 mt-2 text-xs opacity-75 flex flex-col gap-2">
|
||||
{typeParams.length > 0 && (
|
||||
<div>
|
||||
<Highlight
|
||||
hide={[0, 1 + typeParams.length]}
|
||||
>{`class Thing<\n${typeParams.join(
|
||||
",\n",
|
||||
)}\n]> {}`}</Highlight>
|
||||
</div>
|
||||
)}
|
||||
{paramTypes.length > 0 && (
|
||||
<div>
|
||||
<Highlight
|
||||
hide={[0, 1 + paramTypes.length]}
|
||||
>{`function fn(...args: [\n${paramTypes.join(
|
||||
",\n",
|
||||
)}\n]) {}`}</Highlight>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="lg:flex gap-2">
|
||||
<div className="ml-4 mt-2 flex-[3]">
|
||||
<DocComment>{doc || "⚠️ undocumented"}</DocComment>
|
||||
</div>
|
||||
{example && (
|
||||
<div className="flex-[1] relative w-full overflow-x-scroll col-span-2 pl-4 md:pl-0 mt-6 md:mt-0 text-xs opacity-60 group-hover:opacity-100">
|
||||
<div className="opacity-30 text-xs -mb-4">Example:</div>
|
||||
{example}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PropCategory({
|
||||
name,
|
||||
description,
|
||||
example,
|
||||
}: {
|
||||
name: string;
|
||||
description?: ReactNode;
|
||||
example?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-6 mt-8 -mb-4 text-[0.7em] uppercase font-medium tracking-widest opacity-50">
|
||||
{name}
|
||||
</div>
|
||||
{description && <PropDecl doc={description} example={example} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocComment({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="prose-inner-sm max-w-xl leading-snug">{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewCoValueExplainer({ type }: { type: string }) {
|
||||
return (
|
||||
<>
|
||||
Creates a new <ClassRef name={type} /> with the given initial
|
||||
values. The <ClassRef name={type} /> is immediately persisted
|
||||
locally and synced to connected peers. Access rights are determined
|
||||
by roles in the owner <ClassRef name="Group" /> or directly by the
|
||||
owner <ClassRef name="Account" />.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RefValueExplainer({ propName }: { propName: string }) {
|
||||
return (
|
||||
<>
|
||||
Note that even non-optional <PropRef on="co" prop="ref(...)" />{" "}
|
||||
{propName} might be <Highlight>null</Highlight> if the referenced
|
||||
value isn't loaded yet. Accessing one will cause it to be
|
||||
loaded if done from inside a <i>Subscription Context</i>.
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,10 @@ export function Slogan(props: { children: ReactNode; small?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"leading-snug mb-5 max-w-3xl text-stone-700 dark:text-stone-200",
|
||||
props.small ? "text-lg lg:text-xl -mt-2" : "text-xl lg:text-2xl -mt-5",
|
||||
"leading-snug tracking-tight mb-5 max-w-4xl text-stone-700 dark:text-stone-500",
|
||||
props.small
|
||||
? "text-lg lg:text-xl -mt-2"
|
||||
: "text-3xl lg:text-4xl -mt-5",
|
||||
].join(" ")}
|
||||
>
|
||||
{props.children}
|
||||
@@ -21,9 +23,9 @@ export function Grid({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
|
||||
"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",
|
||||
"mt-10 items-stretch",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -43,14 +45,16 @@ export function GridFeature(props: {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"p-4 flex items-center gap-2",
|
||||
"p-4 flex flex-col items-center justify-center gap-2",
|
||||
"not-prose text-base",
|
||||
"border border-stone-200 dark:border-stone-800 rounded-xl shadow-sm",
|
||||
"border border-stone-200 dark:border-stone-800 rounded-xl",
|
||||
props.className || "",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="text-stone-500 mr-2">{props.icon}</div>
|
||||
{props.children}
|
||||
<div className="text-stone-700 dark:text-stone-300">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +63,7 @@ export function GridCard(props: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
|
||||
"col-span-2 p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
|
||||
"border border-stone-200 dark:border-stone-800 rounded-xl shadow-sm",
|
||||
props.className,
|
||||
].join(" ")}
|
||||
@@ -69,26 +73,34 @@ export function GridCard(props: { children: ReactNode; className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function MultiplayerIcon() {
|
||||
export function MultiplayerIcon({
|
||||
color,
|
||||
strokeWidth,
|
||||
size,
|
||||
}: {
|
||||
color?: string;
|
||||
strokeWidth?: number;
|
||||
size: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-8 h-8 -my-1 -mr-2 relative z-0">
|
||||
<div className="relative z-0" style={{ width: size, height: size }}>
|
||||
<MousePointer2Icon
|
||||
size="20"
|
||||
absoluteStrokeWidth
|
||||
strokeWidth={2}
|
||||
className="absolute top-1 right-0"
|
||||
size={0.6 * size}
|
||||
strokeWidth={(strokeWidth || 1) / 0.6}
|
||||
color={color}
|
||||
className="absolute top-0 right-0"
|
||||
/>
|
||||
<MousePointer2Icon
|
||||
size="16"
|
||||
absoluteStrokeWidth
|
||||
strokeWidth={2}
|
||||
className="absolute bottom-1 left-0 -scale-x-100"
|
||||
size={0.5 * size}
|
||||
strokeWidth={(strokeWidth || 1) / 0.5}
|
||||
color={color}
|
||||
className="absolute bottom-0 left-0 -scale-x-100"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ComingSoonBadge({when = "soon"}: {when?: string}) {
|
||||
export function ComingSoonBadge({ when = "soon" }: { when?: string }) {
|
||||
return (
|
||||
<span className="bg-stone-100 dark:bg-stone-900 text-stone-500 dark:text-stone-400 border border-stone-300 dark:border-stone-700 text-[0.6rem] px-1 py-0.5 rounded-xl align-text-top">
|
||||
Coming {when}
|
||||
@@ -102,7 +114,7 @@ import { HandIcon, MousePointer2Icon, TextCursorIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ResponsiveIframe(
|
||||
props: IframeHTMLAttributes<HTMLIFrameElement>
|
||||
props: IframeHTMLAttributes<HTMLIFrameElement> & { localSrc: string },
|
||||
) {
|
||||
return <ResponsiveIframeClient {...props} />;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user