Compare commits
273 Commits
cojson@0.0
...
cojson@0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
934fe4d29b | ||
|
|
408012f2e5 | ||
|
|
d0078b830e | ||
|
|
e52948b2b7 | ||
|
|
53bb1b230b | ||
|
|
54e83aeaaa | ||
|
|
aa3129cab5 | ||
|
|
90520dddd7 | ||
|
|
03eb77070a | ||
|
|
4ba5c255b6 | ||
|
|
01817db873 | ||
|
|
46fcbd6c01 | ||
|
|
aa3e3de09e | ||
|
|
af3d48764d | ||
|
|
091f36b736 | ||
|
|
7107f79f42 | ||
|
|
9922db2336 | ||
|
|
75db570198 | ||
|
|
28a09f377b | ||
|
|
fd2e0855bb | ||
|
|
82e1d57bd6 | ||
|
|
a2fbb0b0c8 | ||
|
|
8feddf9932 | ||
|
|
feed34b1cf | ||
|
|
662c980cf2 | ||
|
|
f5ae530890 | ||
|
|
46bf7dd3ce | ||
|
|
5d4eb38204 | ||
|
|
66da658075 | ||
|
|
3477b74573 | ||
|
|
f3de4906b7 | ||
|
|
caded3f189 | ||
|
|
5196395495 | ||
|
|
8089a7ed9f | ||
|
|
99230d31d2 | ||
|
|
94bca03f59 | ||
|
|
49719b6e6d | ||
|
|
1bdb781452 | ||
|
|
c336f69a6b | ||
|
|
c8cb1ce208 | ||
|
|
814a6a80cd | ||
|
|
5fdfe18b32 | ||
|
|
7b7a74778b | ||
|
|
39dbd46556 | ||
|
|
1db4a14be4 | ||
|
|
4a4ea4e196 | ||
|
|
e0724441eb | ||
|
|
5d47895515 | ||
|
|
c1dfac7260 | ||
|
|
bf29cb3bae | ||
|
|
a0a9b3f851 | ||
|
|
4c4deb22c9 | ||
|
|
a42c497055 | ||
|
|
f1dcdb20bc | ||
|
|
46330ae201 | ||
|
|
bfe3595b4c | ||
|
|
34c39e6a55 | ||
|
|
5a85501919 | ||
|
|
97a4282e5e | ||
|
|
39c13b50a3 | ||
|
|
ad304e321b | ||
|
|
8c0b2da461 | ||
|
|
72fce45b2b | ||
|
|
1f49d7fda6 | ||
|
|
eec8ee7027 | ||
|
|
188eb2e1e3 | ||
|
|
62867b32d9 | ||
|
|
ccebd2447d | ||
|
|
08dca75789 | ||
|
|
16b3e1381b | ||
|
|
f1cd639a09 | ||
|
|
be18e4de14 | ||
|
|
7e62c91d44 | ||
|
|
b2d5a103b5 | ||
|
|
4ee2cad39e | ||
|
|
b7c8a0038b | ||
|
|
8c27e8c379 | ||
|
|
0133aa47ff | ||
|
|
5659c925a2 | ||
|
|
27779ac792 | ||
|
|
3f1bfa4629 | ||
|
|
15a693c3ed | ||
|
|
b1d620e145 | ||
|
|
478fbd0aa9 | ||
|
|
ee906b7351 | ||
|
|
dd15f21ccb | ||
|
|
d7cd5fda7c | ||
|
|
174300b00f | ||
|
|
b2c8d8c855 | ||
|
|
2bad2b6bfe | ||
|
|
880d0ff855 | ||
|
|
e66cbee6cd | ||
|
|
03e470721e | ||
|
|
ecf73bcfa7 | ||
|
|
2c3a500286 | ||
|
|
8b83061cf4 | ||
|
|
e75c3207d6 | ||
|
|
41d4b5ba0b | ||
|
|
21fa1b168b | ||
|
|
91e5e7f2ab | ||
|
|
e3f7e2f1bd | ||
|
|
084cf80c60 | ||
|
|
632e3bbb08 | ||
|
|
17d17833b2 | ||
|
|
8e22bd9c1e | ||
|
|
98213743f3 | ||
|
|
bb855ed83d | ||
|
|
a8ef49e228 | ||
|
|
e0ad32dbd2 | ||
|
|
62bf769cad | ||
|
|
7488ff25b2 | ||
|
|
b69c9da983 | ||
|
|
d30fdef8aa | ||
|
|
9c5a6b9833 | ||
|
|
d300d265c4 | ||
|
|
1d72ce587f | ||
|
|
3fdb41dcb9 | ||
|
|
f20de2f04a | ||
|
|
31b31f111b | ||
|
|
2ae9fb9778 | ||
|
|
cd0da0f6bf | ||
|
|
cd9bfbb9fa | ||
|
|
ed0428bf97 | ||
|
|
c038a02051 | ||
|
|
31abcfeef4 | ||
|
|
5f32d9ccf5 | ||
|
|
0510600104 | ||
|
|
7f30fbf3c5 | ||
|
|
3d56260ca4 | ||
|
|
1137775da9 | ||
|
|
3951fdc938 | ||
|
|
5779e357dd | ||
|
|
2842d80f26 | ||
|
|
96387d8023 | ||
|
|
6720c19233 | ||
|
|
ef732b4700 | ||
|
|
ee7e3ee5a7 | ||
|
|
ceeed88fa5 | ||
|
|
79353a1d97 | ||
|
|
7fdc42c62f | ||
|
|
3a2e854a88 | ||
|
|
661a2d023a | ||
|
|
6ef5b6b2ab | ||
|
|
1384ebed84 | ||
|
|
17e53f9998 | ||
|
|
cfb1f39efe | ||
|
|
2234276dcf | ||
|
|
bb0a6a0600 | ||
|
|
0a6eb0c10a | ||
|
|
88b67d89e0 | ||
|
|
1a65d826b2 | ||
|
|
6c65ec2b46 | ||
|
|
5b578a832d | ||
|
|
042afc52d7 | ||
|
|
1b83493964 | ||
|
|
3b50da1a74 | ||
|
|
8e0fc74d9f | ||
|
|
e28326f32c | ||
|
|
d7e8b0b9da | ||
|
|
c46a1f6b0a | ||
|
|
7947918278 | ||
|
|
50c36e7255 | ||
|
|
c39a7ed1b7 | ||
|
|
83762dbb0f | ||
|
|
7c82e12508 | ||
|
|
6db149be36 | ||
|
|
909a101f99 | ||
|
|
df0b6fe138 | ||
|
|
0543756016 | ||
|
|
92eae0e180 | ||
|
|
9ccc97fcd3 | ||
|
|
120ba57274 | ||
|
|
0679a64002 | ||
|
|
e9d561adbd | ||
|
|
bb5fd24f6a | ||
|
|
18d5b9146f | ||
|
|
39850d465f | ||
|
|
27e0d6df46 | ||
|
|
6d0c820724 | ||
|
|
78a1d5a614 | ||
|
|
33c2705329 | ||
|
|
4873a634a4 | ||
|
|
edb43cd070 | ||
|
|
b128a2d6f7 | ||
|
|
27abcb4f6f | ||
|
|
e9b41c4344 | ||
|
|
d93b376e4b | ||
|
|
aeb38eb7d5 | ||
|
|
07bffb5050 | ||
|
|
012bd43865 | ||
|
|
ffc1181b81 | ||
|
|
4ca5e258b5 | ||
|
|
2255c824b7 | ||
|
|
8ed59e40e9 | ||
|
|
03b34b4b66 | ||
|
|
53c93f6a0b | ||
|
|
4af7f25eab | ||
|
|
6d6e8a0e28 | ||
|
|
4a617c8323 | ||
|
|
eaed275a79 | ||
|
|
01fdcaed34 | ||
|
|
7aeb1a789b | ||
|
|
a00649fa29 | ||
|
|
764954c727 | ||
|
|
b0ec93eb3a | ||
|
|
4dd226bc95 | ||
|
|
1692340856 | ||
|
|
fbda78f908 | ||
|
|
61e9f6afad | ||
|
|
246bbb119d | ||
|
|
80054515c9 | ||
|
|
f9486a82c3 | ||
|
|
d0babab822 | ||
|
|
ab34172e01 | ||
|
|
b779a91611 | ||
|
|
297a8646dd | ||
|
|
25eb3e097f | ||
|
|
fe1092ccf6 | ||
|
|
29abbc455c | ||
|
|
f6864e0f93 | ||
|
|
9440b5306c | ||
|
|
aa34f1e8a6 | ||
|
|
24ce7dbdf1 | ||
|
|
65a7a66c15 | ||
|
|
0f999a2c2d | ||
|
|
2247c97080 | ||
|
|
cbdc722959 | ||
|
|
bb157b6099 | ||
|
|
e1f8ec6f11 | ||
|
|
9854238346 | ||
|
|
3b5ab90006 | ||
|
|
988dc37902 | ||
|
|
4ef4b87d95 | ||
|
|
27f811b9e9 | ||
|
|
52be603996 | ||
|
|
d1123866c2 | ||
|
|
9750fbee68 | ||
|
|
2f91184201 | ||
|
|
97badc24fb | ||
|
|
eaeb201f10 | ||
|
|
9c5dd96f58 | ||
|
|
a1a96e1118 | ||
|
|
40b4ebaf00 | ||
|
|
37559b2dec | ||
|
|
81fd3e8aff | ||
|
|
f1747e1aaf | ||
|
|
934365c24d | ||
|
|
ff20c3a260 | ||
|
|
1d0ce83019 | ||
|
|
2951d8452f | ||
|
|
6532e79790 | ||
|
|
09603d17a3 | ||
|
|
10b372da6e | ||
|
|
b556a36db3 | ||
|
|
094c505cf0 | ||
|
|
4d71ab8aac | ||
|
|
f1297c613b | ||
|
|
1ee5c2b3c8 | ||
|
|
f14337f862 | ||
|
|
b8f4571474 | ||
|
|
6557532743 | ||
|
|
ea6835664b | ||
|
|
a00332f4af | ||
|
|
6a6fb2eb3c | ||
|
|
0437223d50 | ||
|
|
30c7e1bf6d | ||
|
|
aaa9d876d5 | ||
|
|
264009a1a9 | ||
|
|
f2cb5d1b59 | ||
|
|
8610db2d8e | ||
|
|
c672a03338 | ||
|
|
cb60088d2a | ||
|
|
a59d5d3b70 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||||
|
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||||
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||||
|
|
||||||
|
We have a quick list of common questions to get you started engaging with this project in
|
||||||
|
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$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": []
|
||||||
|
}
|
||||||
135
.github/workflows/build-and-deploy.yaml
vendored
135
.github/workflows/build-and-deploy.yaml
vendored
@@ -7,8 +7,12 @@ on:
|
|||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-examples:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# example: ["chat", "todo", "pets", "twit", "file-drop"]
|
||||||
|
example: ["twit", "chat"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -17,40 +21,78 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- name: Nuke Workspace
|
|
||||||
run: |
|
|
||||||
rm package.json yarn.lock;
|
|
||||||
- name: Yarn Build
|
|
||||||
run: |
|
|
||||||
yarn install --frozen-lockfile;
|
|
||||||
yarn build;
|
|
||||||
working-directory: ./examples/todo
|
|
||||||
|
|
||||||
- uses: satackey/action-docker-layer-caching@v0.0.11
|
- name: Set up Docker Buildx
|
||||||
continue-on-error: true
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
|
||||||
key: docker-layer-caching-${{ github.workflow }}-{hash}
|
|
||||||
restore-keys: |
|
|
||||||
docker-layer-caching-${{ github.workflow }}-
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: gardencmp
|
username: gardencmp
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Docker Build & Push
|
- name: Nuke Workspace
|
||||||
run: |
|
run: |
|
||||||
export DOCKER_TAG=ghcr.io/gardencmp/jazz-example-todo:${{github.head_ref || github.ref_name}}-${{github.sha}}-$(date +%s) ;
|
rm package.json yarn.lock;
|
||||||
docker build . --file Dockerfile --tag $DOCKER_TAG;
|
|
||||||
docker push $DOCKER_TAG;
|
|
||||||
echo "DOCKER_TAG=$DOCKER_TAG" >> $GITHUB_ENV
|
|
||||||
working-directory: ./examples/todo
|
|
||||||
|
|
||||||
|
- name: Yarn Build
|
||||||
|
run: |
|
||||||
|
yarn install --frozen-lockfile;
|
||||||
|
yarn build;
|
||||||
|
working-directory: ./examples/${{ matrix.example }}
|
||||||
|
|
||||||
|
- name: Docker Build & Push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./examples/${{ matrix.example }}
|
||||||
|
push: true
|
||||||
|
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-example-${{ matrix.example }}:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
# build-homepage:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v3
|
||||||
|
# with:
|
||||||
|
# submodules: true
|
||||||
|
|
||||||
|
# - name: Set up Docker Buildx
|
||||||
|
# uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
# - name: Login to GitHub Container Registry
|
||||||
|
# uses: docker/login-action@v2
|
||||||
|
# with:
|
||||||
|
# registry: ghcr.io
|
||||||
|
# username: gardencmp
|
||||||
|
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Docker Build & Push
|
||||||
|
# uses: docker/build-push-action@v4
|
||||||
|
# with:
|
||||||
|
# context: ./homepage/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
|
||||||
|
|
||||||
|
deploy-examples:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-examples
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# example: ["chat", "todo", "pets", "twit", "file-drop"]
|
||||||
|
example: ["twit", "chat"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
- uses: gacts/install-nomad@v1
|
- uses: gacts/install-nomad@v1
|
||||||
- name: Tailscale
|
- name: Tailscale
|
||||||
uses: tailscale/github-action@v1
|
uses: tailscale/github-action@v1
|
||||||
@@ -69,13 +111,42 @@ jobs:
|
|||||||
|
|
||||||
export DOCKER_USER=gardencmp;
|
export DOCKER_USER=gardencmp;
|
||||||
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||||
export DOCKER_TAG=${{ env.DOCKER_TAG }};
|
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-example-${{ matrix.example }}:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
|
||||||
|
|
||||||
for region in ${{ vars.DEPLOY_REGIONS }}
|
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||||
do
|
cat job-instance.nomad;
|
||||||
export REGION=$region;
|
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN} ${REGION}' < job-template.nomad > job-instance.nomad;
|
working-directory: ./examples/${{ matrix.example }}
|
||||||
cat job-instance.nomad;
|
|
||||||
NOMAD_ADDR='${{ secrets.NOMAD_ADDR }}' nomad job run job-instance.nomad;
|
# deploy-homepage:
|
||||||
done
|
# runs-on: ubuntu-latest
|
||||||
working-directory: ./examples/todo
|
# needs: build-homepage
|
||||||
|
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v3
|
||||||
|
# with:
|
||||||
|
# submodules: true
|
||||||
|
# - uses: gacts/install-nomad@v1
|
||||||
|
# - name: Tailscale
|
||||||
|
# uses: tailscale/github-action@v1
|
||||||
|
# with:
|
||||||
|
# authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
|
||||||
|
|
||||||
|
# - name: Deploy on Nomad
|
||||||
|
# run: |
|
||||||
|
# if [ "${{github.ref_name}}" == "main" ]; then
|
||||||
|
# export BRANCH_SUFFIX="";
|
||||||
|
# export BRANCH_SUBDOMAIN="";
|
||||||
|
# else
|
||||||
|
# export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
|
||||||
|
# export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# export DOCKER_USER=gardencmp;
|
||||||
|
# export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||||
|
# export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
|
||||||
|
|
||||||
|
# envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||||
|
# cat job-instance.nomad;
|
||||||
|
# NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||||
|
# working-directory: ./homepage/homepage-jazz
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
lerna-debug.log
|
lerna-debug.log
|
||||||
|
docsTmp
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
117
README.md
117
README.md
@@ -1,9 +1,116 @@
|
|||||||
# Jazz - instant sync
|
# Jazz - instant sync
|
||||||
|
|
||||||
Jazz is an open-source toolkit for telepathic data.
|
<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>
|
||||||
|
|
||||||
Ship faster and simplify frontend, backend & devops by building with Telepathic Data.
|
**Jazz is an open-source toolkit for building apps with *secure sync.***
|
||||||
Get real-time multiplayer and cross-device sync for free.
|
|
||||||
|
|
||||||
## What is Telepathic Data?
|
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 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
|
||||||
|
|
||||||
|
## Example App Walkthrough
|
||||||
|
|
||||||
|
**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.
|
||||||
18
examples/chat/.eslintrc.cjs
Normal file
18
examples/chat/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
examples/chat/.gitignore
vendored
Normal file
24
examples/chat/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
9
examples/chat/CHANGELOG.md
Normal file
9
examples/chat/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# jazz-example-chat
|
||||||
|
|
||||||
|
## 0.0.46
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- jazz-react@0.5.0
|
||||||
|
- jazz-react-auth-local@0.4.16
|
||||||
4
examples/chat/Dockerfile
Normal file
4
examples/chat/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM caddy:2.7.3-alpine
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||||
|
|
||||||
|
COPY ./dist /usr/share/caddy/
|
||||||
64
examples/chat/README.md
Normal file
64
examples/chat/README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 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).
|
||||||
14
examples/chat/index.html
Normal file
14
examples/chat/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||||
|
<link rel="stylesheet" href="/src/index.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jazz Chat Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/app.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
examples/chat/job-template.nomad
Normal file
56
examples/chat/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
job "chat$BRANCH_SUFFIX" {
|
||||||
|
region = "global"
|
||||||
|
datacenters = ["*"]
|
||||||
|
|
||||||
|
group "static" {
|
||||||
|
count = 4
|
||||||
|
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
to = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
attribute = "${node.class}"
|
||||||
|
operator = "="
|
||||||
|
value = "mesh"
|
||||||
|
}
|
||||||
|
|
||||||
|
spread {
|
||||||
|
attribute = "${node.datacenter}"
|
||||||
|
weight = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
distinct_hosts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
task "server" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "$DOCKER_TAG"
|
||||||
|
ports = ["http"]
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
username = "$DOCKER_USER"
|
||||||
|
password = "$DOCKER_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service {
|
||||||
|
tags = ["public"]
|
||||||
|
name = "chat$BRANCH_SUFFIX"
|
||||||
|
port = "http"
|
||||||
|
provider = "consul"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
cpu = 50 # MHz
|
||||||
|
memory = 50 # MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# deploy bump 4
|
||||||
48
examples/chat/package.json
Normal file
48
examples/chat/package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "jazz-example-chat",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.46",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
|
"@types/qrcode": "^1.5.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"hash-slash": "^0.1.3",
|
||||||
|
"jazz-react": "^0.5.0",
|
||||||
|
"jazz-react-auth-local": "^0.4.16",
|
||||||
|
"lucide-react": "^0.274.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router": "^6.16.0",
|
||||||
|
"react-router-dom": "^6.16.0",
|
||||||
|
"react-use": "^17.4.0",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uniqolor": "^1.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
examples/chat/postcss.config.js
Normal file
6
examples/chat/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
examples/chat/public/jazz-logo.png
Normal file
BIN
examples/chat/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
35
examples/chat/src/app.tsx
Normal file
35
examples/chat/src/app.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
43
examples/chat/src/chatWindow.tsx
Normal file
43
examples/chat/src/chatWindow.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useAutoSub } from 'jazz-react';
|
||||||
|
import { Chat, Message } from './dataModel.ts';
|
||||||
|
|
||||||
|
export function ChatWindow(props: { chatId: Chat['id'] }) {
|
||||||
|
const chat = useAutoSub(props.chatId);
|
||||||
|
|
||||||
|
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
|
||||||
|
{
|
||||||
|
chat.map((msg, i) => (
|
||||||
|
<ChatBubble key={msg?.id}
|
||||||
|
text={msg?.text}
|
||||||
|
by={chat.meta.edits[i].by?.profile?.name}
|
||||||
|
byMe={chat.meta.edits[i].by?.isMe}
|
||||||
|
at={chat.meta.edits[i].at} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<ChatInput onSubmit={(text) => {
|
||||||
|
const msg = chat.meta.group.createMap<Message>({ text });
|
||||||
|
chat.append(msg.id);
|
||||||
|
}}/>
|
||||||
|
</div> : <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatBubble(props: { text?: string, by?: string, at?: Date, byMe?: boolean }) {
|
||||||
|
return <div className={`${props.byMe ? 'items-end' : 'items-start'} flex flex-col`}>
|
||||||
|
<div className='rounded-xl bg-stone-100 dark:bg-stone-700 dark:text-white py-2 px-4 mt-2 min-w-[5rem]'>
|
||||||
|
{ props.text }
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-neutral-500 ml-2'>
|
||||||
|
{ props.by } { props.at?.getHours() }:{ props.at?.getMinutes() }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatInput(props: { onSubmit: (text: string) => void }) {
|
||||||
|
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white dark:border-stone-700'
|
||||||
|
placeholder='Type a message and press Enter'
|
||||||
|
onKeyDown={({ key, currentTarget: input }) => {
|
||||||
|
if (key !== 'Enter' || !input.value) return;
|
||||||
|
props.onSubmit(input.value);
|
||||||
|
input.value = '';
|
||||||
|
}}/>
|
||||||
|
}
|
||||||
4
examples/chat/src/dataModel.ts
Normal file
4
examples/chat/src/dataModel.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { CoMap, CoList } from 'cojson';
|
||||||
|
|
||||||
|
export type Chat = CoList<Message['id']>;
|
||||||
|
export type Message = CoMap<{ text: string }>;
|
||||||
78
examples/chat/src/index.css
Normal file
78
examples/chat/src/index.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--primary: 24 9.8% 10%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--primary: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 24 5.7% 82.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
examples/chat/src/vite-env.d.ts
vendored
Normal file
1
examples/chat/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
75
examples/chat/tailwind.config.js
Normal file
75
examples/chat/tailwind.config.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}
|
||||||
29
examples/chat/tsconfig.json
Normal file
29
examples/chat/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/chat/tsconfig.node.json
Normal file
10
examples/chat/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
16
examples/chat/vite.config.ts
Normal file
16
examples/chat/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
minify: false
|
||||||
|
}
|
||||||
|
})
|
||||||
18
examples/file-drop/.eslintrc.cjs
Normal file
18
examples/file-drop/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
examples/file-drop/.gitignore
vendored
Normal file
24
examples/file-drop/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
9
examples/file-drop/CHANGELOG.md
Normal file
9
examples/file-drop/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# jazz-example-file-drop
|
||||||
|
|
||||||
|
## 0.0.63
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- jazz-react@0.5.0
|
||||||
|
- jazz-react-auth-local@0.4.16
|
||||||
4
examples/file-drop/Dockerfile
Normal file
4
examples/file-drop/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM caddy:2.7.3-alpine
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||||
|
|
||||||
|
COPY ./dist /usr/share/caddy/
|
||||||
64
examples/file-drop/README.md
Normal file
64
examples/file-drop/README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 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).
|
||||||
16
examples/file-drop/components.json
Normal file
16
examples/file-drop/components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/basicComponents",
|
||||||
|
"utils": "@/basicComponents/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
examples/file-drop/index.html
Normal file
13
examples/file-drop/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jazz File Drop Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/2_main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
examples/file-drop/job-template.nomad
Normal file
56
examples/file-drop/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
job "example-file-drop$BRANCH_SUFFIX" {
|
||||||
|
region = "global"
|
||||||
|
datacenters = ["*"]
|
||||||
|
|
||||||
|
group "static" {
|
||||||
|
count = 4
|
||||||
|
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
to = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
attribute = "${node.class}"
|
||||||
|
operator = "="
|
||||||
|
value = "mesh"
|
||||||
|
}
|
||||||
|
|
||||||
|
spread {
|
||||||
|
attribute = "${node.datacenter}"
|
||||||
|
weight = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
distinct_hosts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
task "server" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "$DOCKER_TAG"
|
||||||
|
ports = ["http"]
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
username = "$DOCKER_USER"
|
||||||
|
password = "$DOCKER_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service {
|
||||||
|
tags = ["public"]
|
||||||
|
name = "example-file-drop$BRANCH_SUFFIX"
|
||||||
|
port = "http"
|
||||||
|
provider = "consul"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
cpu = 50 # MHz
|
||||||
|
memory = 50 # MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# deploy bump 4
|
||||||
46
examples/file-drop/package.json
Normal file
46
examples/file-drop/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "jazz-example-file-drop",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.63",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 6610",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
|
"@types/qrcode": "^1.5.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"jazz-react": "^0.5.0",
|
||||||
|
"jazz-react-auth-local": "^0.4.16",
|
||||||
|
"lucide-react": "^0.274.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router": "^6.16.0",
|
||||||
|
"react-router-dom": "^6.16.0",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uniqolor": "^1.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
examples/file-drop/postcss.config.js
Normal file
6
examples/file-drop/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
examples/file-drop/public/jazz-logo.png
Normal file
BIN
examples/file-drop/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
5
examples/file-drop/src/1_types.ts
Normal file
5
examples/file-drop/src/1_types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { CoMap, BinaryCoStream } from "cojson";
|
||||||
|
|
||||||
|
export type FileBundle = CoMap<{
|
||||||
|
[filename: string]: BinaryCoStream['id']
|
||||||
|
}>;
|
||||||
187
examples/file-drop/src/2_main.tsx
Normal file
187
examples/file-drop/src/2_main.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import React, { ChangeEvent, useCallback, useState } from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import {
|
||||||
|
RouterProvider,
|
||||||
|
createHashRouter,
|
||||||
|
useNavigate,
|
||||||
|
useParams,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
import { WithJazz, useJazz, useAcceptInvite, useAutoSub } from "jazz-react";
|
||||||
|
import { LocalAuth } from "jazz-react-auth-local";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
ThemeProvider,
|
||||||
|
TitleAndLogo,
|
||||||
|
} from "./basicComponents/index.ts";
|
||||||
|
import { PrettyAuthUI } from "./components/Auth.tsx";
|
||||||
|
import { FileBundle } from "./1_types.ts";
|
||||||
|
import {
|
||||||
|
createBinaryStreamFromBlob,
|
||||||
|
readBlobFromBinaryStream,
|
||||||
|
} from "jazz-browser";
|
||||||
|
import { DownloadIcon } from "lucide-react";
|
||||||
|
|
||||||
|
const appName = "Jazz File Drop Example";
|
||||||
|
|
||||||
|
const auth = LocalAuth({
|
||||||
|
appName,
|
||||||
|
Component: PrettyAuthUI,
|
||||||
|
});
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<App />
|
||||||
|
</WithJazz>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// logOut logs out the AuthProvider passed to `<WithJazz/>` above.
|
||||||
|
const { logOut } = useJazz();
|
||||||
|
|
||||||
|
const router = createHashRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <FileDropUI />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/bundle/:bundleId",
|
||||||
|
element: <FileDropUIPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/invite/*",
|
||||||
|
element: <p>Accepting invite...</p>,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// `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((bundleId) => router.navigate("/v/" + bundleId));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => router.navigate("/").then(logOut)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileDropUIPage() {
|
||||||
|
const { bundleId } = useParams<{ bundleId: FileBundle["id"] }>();
|
||||||
|
|
||||||
|
return <FileDropUI bundleId={bundleId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileDropUI({ bundleId }: { bundleId?: FileBundle["id"] }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { me, localNode } = useJazz();
|
||||||
|
const fileBundle = useAutoSub(bundleId);
|
||||||
|
|
||||||
|
const [progressMessage, setProgressMessage] = useState<{
|
||||||
|
[name: string]: string;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let fileBundleToUse = fileBundle?.meta.coValue;
|
||||||
|
let isFirstUpload = false;
|
||||||
|
|
||||||
|
if (!fileBundleToUse) {
|
||||||
|
const group = me.createGroup().addMember("everyone", "reader");
|
||||||
|
fileBundleToUse = group.createMap<FileBundle>();
|
||||||
|
isFirstUpload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [...(event.target.files || [])];
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
files.map((file) =>
|
||||||
|
createBinaryStreamFromBlob(
|
||||||
|
file,
|
||||||
|
fileBundleToUse!.group,
|
||||||
|
{ type: "binary" },
|
||||||
|
(progress) =>
|
||||||
|
setProgressMessage((old) => ({
|
||||||
|
...old,
|
||||||
|
[file.name]: `Creating ${Math.round(
|
||||||
|
progress * 100
|
||||||
|
)}%`,
|
||||||
|
}))
|
||||||
|
).then((stream) => {
|
||||||
|
fileBundleToUse!.set(file.name, stream.id);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
if (isFirstUpload) {
|
||||||
|
navigate("/bundle/" + fileBundleToUse!.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.target.value = "";
|
||||||
|
},
|
||||||
|
[me, navigate, fileBundle]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-full p-5 w-[40rem]">
|
||||||
|
<h1 className="text-3xl font-bold mb-5">File Drop</h1>
|
||||||
|
{[
|
||||||
|
...new Set([
|
||||||
|
...Object.keys(fileBundle || {}),
|
||||||
|
...Object.keys(progressMessage),
|
||||||
|
]),
|
||||||
|
].map((name) => (
|
||||||
|
<div className="mb-5 flex justify-between" key={name}>
|
||||||
|
{name} {progressMessage[name]}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={!(name in (fileBundle || {}))}
|
||||||
|
onClick={() => {
|
||||||
|
const streamId = fileBundle?.meta.coValue.get(name);
|
||||||
|
streamId &&
|
||||||
|
readBlobFromBinaryStream(
|
||||||
|
streamId,
|
||||||
|
localNode,
|
||||||
|
false,
|
||||||
|
(progress) =>
|
||||||
|
setProgressMessage((old) => ({
|
||||||
|
...old,
|
||||||
|
[name]: `Loading ${Math.round(
|
||||||
|
progress * 100
|
||||||
|
)}%`,
|
||||||
|
}))
|
||||||
|
).then((blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
window.open(url, "_blank");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!fileBundle || fileBundle.meta.group.myRole() === "admin") && (
|
||||||
|
<Input type="file" onChange={onChange} multiple />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Walkthrough: Continue with ./3_NewProjectForm.tsx */
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/basicComponents/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/basicComponents/ui/button";
|
||||||
|
|
||||||
export function SubmittableInput({
|
export function SubmittableInput({
|
||||||
onSubmit, label, placeholder,
|
onSubmit,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: (text: string) => void;
|
onSubmit: (text: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -21,12 +25,14 @@ export function SubmittableInput({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
className="-ml-3 -my-2 flex-grow flex-3"
|
className="-ml-3 -my-2 flex-grow flex-3 text-base"
|
||||||
name="text"
|
name="text"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoComplete="off" />
|
autoComplete="off"
|
||||||
<Button asChild type="submit" className="flex-shrink flex-1">
|
disabled={disabled}
|
||||||
<Input type="submit" value={label} />
|
/>
|
||||||
|
<Button asChild type="submit" className="flex-shrink flex-1 cursor-pointer">
|
||||||
|
<Input type="submit" value={label} disabled={disabled} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
10
examples/file-drop/src/basicComponents/TitleAndLogo.tsx
Normal file
10
examples/file-drop/src/basicComponents/TitleAndLogo.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Toaster } from ".";
|
||||||
|
|
||||||
|
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 />
|
||||||
|
</>
|
||||||
|
}
|
||||||
17
examples/file-drop/src/basicComponents/index.ts
Normal file
17
examples/file-drop/src/basicComponents/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export { Button } from "./ui/button";
|
||||||
|
export { Checkbox } from "./ui/checkbox";
|
||||||
|
export { Input } from "./ui/input";
|
||||||
|
export { Skeleton } from "./ui/skeleton";
|
||||||
|
export { Toaster } from "./ui/toaster";
|
||||||
|
export { useToast } from "./ui/use-toast";
|
||||||
|
export { SubmittableInput } from "./SubmittableInput";
|
||||||
|
export { TitleAndLogo } from "./TitleAndLogo";
|
||||||
|
export { ThemeProvider } from "./themeProvider";
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "./ui/table";
|
||||||
@@ -2,7 +2,7 @@ import * as React from "react"
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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",
|
"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",
|
||||||
@@ -2,7 +2,7 @@ import * as React from "react"
|
|||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
import { Check } from "lucide-react"
|
import { Check } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
15
examples/file-drop/src/basicComponents/ui/skeleton.tsx
Normal file
15
examples/file-drop/src/basicComponents/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
const Table = React.forwardRef<
|
const Table = React.forwardRef<
|
||||||
HTMLTableElement,
|
HTMLTableElement,
|
||||||
127
examples/file-drop/src/basicComponents/ui/toast.tsx
Normal file
127
examples/file-drop/src/basicComponents/ui/toast.tsx
Normal file
@@ -0,0 +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 { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
33
examples/file-drop/src/basicComponents/ui/toaster.tsx
Normal file
33
examples/file-drop/src/basicComponents/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/basicComponents/ui/toast"
|
||||||
|
import { useToast } from "@/basicComponents/ui/use-toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
192
examples/file-drop/src/basicComponents/ui/use-toast.ts
Normal file
192
examples/file-drop/src/basicComponents/ui/use-toast.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/basicComponents/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
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
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_VALUE
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// ! 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.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
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])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { LocalAuthComponent } from "jazz-react-auth-local";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "./ui/input";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
|
|
||||||
export const PrettyAuthComponent: LocalAuthComponent = ({
|
import { LocalAuthComponent } from "jazz-react-auth-local";
|
||||||
|
|
||||||
|
import { Input, Button } from "../basicComponents";
|
||||||
|
|
||||||
|
export const PrettyAuthUI: LocalAuthComponent = ({
|
||||||
loading,
|
loading,
|
||||||
logIn,
|
logIn,
|
||||||
signUp,
|
signUp,
|
||||||
@@ -11,7 +12,7 @@ export const PrettyAuthComponent: LocalAuthComponent = ({
|
|||||||
const [username, setUsername] = useState<string>("");
|
const [username, setUsername] = useState<string>("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center p-5">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -28,6 +29,7 @@ export const PrettyAuthComponent: LocalAuthComponent = ({
|
|||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
autoComplete="webauthn"
|
autoComplete="webauthn"
|
||||||
|
className="text-base"
|
||||||
/>
|
/>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Input
|
<Input
|
||||||
45
examples/file-drop/src/components/InviteButton.tsx
Normal file
45
examples/file-drop/src/components/InviteButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import { useToast, Button } from "../basicComponents";
|
||||||
|
import { CoValue } from "cojson";
|
||||||
|
import { Resolved, createInviteLink } from "jazz-react";
|
||||||
|
|
||||||
|
export function InviteButton<T extends CoValue>({ value }: { value?: Resolved<T> }) {
|
||||||
|
const [existingInviteLink, setExistingInviteLink] = useState<string>();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
value?.meta.group?.myRole() === "admin" && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="py-0"
|
||||||
|
disabled={!value.meta.group || !value.id}
|
||||||
|
variant="outline"
|
||||||
|
onClick={async () => {
|
||||||
|
let inviteLink = existingInviteLink;
|
||||||
|
if (value.meta.group && value.id && !inviteLink) {
|
||||||
|
inviteLink = createInviteLink(value, "writer");
|
||||||
|
setExistingInviteLink(inviteLink);
|
||||||
|
}
|
||||||
|
if (inviteLink) {
|
||||||
|
const qr = await QRCode.toDataURL(inviteLink, {
|
||||||
|
errorCorrectionLevel: "L",
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Copied invite link to clipboard!",
|
||||||
|
description: (
|
||||||
|
<img src={qr} className="w-20 h-20" />
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
76
examples/file-drop/src/index.css
Normal file
76
examples/file-drop/src/index.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--primary: 24 9.8% 10%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--primary: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 24 5.7% 82.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
examples/file-drop/src/vite-env.d.ts
vendored
Normal file
1
examples/file-drop/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
76
examples/file-drop/tailwind.config.js
Normal file
76
examples/file-drop/tailwind.config.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}
|
||||||
29
examples/file-drop/tsconfig.json
Normal file
29
examples/file-drop/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/file-drop/tsconfig.node.json
Normal file
10
examples/file-drop/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
16
examples/file-drop/vite.config.ts
Normal file
16
examples/file-drop/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
minify: false
|
||||||
|
}
|
||||||
|
})
|
||||||
18
examples/pets/.eslintrc.cjs
Normal file
18
examples/pets/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
examples/pets/.gitignore
vendored
Normal file
24
examples/pets/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
10
examples/pets/CHANGELOG.md
Normal file
10
examples/pets/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# jazz-example-pets
|
||||||
|
|
||||||
|
## 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
|
||||||
4
examples/pets/Dockerfile
Normal file
4
examples/pets/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM caddy:2.7.3-alpine
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||||
|
|
||||||
|
COPY ./dist /usr/share/caddy/
|
||||||
51
examples/pets/README.md
Normal file
51
examples/pets/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Jazz Rate-My-Pet List Example
|
||||||
|
|
||||||
|
Live version: https://example-pets.jazz.tools
|
||||||
|
|
||||||
|
## Installing & running the example locally
|
||||||
|
|
||||||
|
Start by checking out just the example app to a folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit gardencmp/jazz/examples/pets jazz-example-pets
|
||||||
|
cd jazz-example-pets
|
||||||
|
```
|
||||||
|
|
||||||
|
(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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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/0_main.tsx](./src/0_main.tsx).
|
||||||
16
examples/pets/components.json
Normal file
16
examples/pets/components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/basicComponents",
|
||||||
|
"utils": "@/basicComponents/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
examples/pets/index.html
Normal file
13
examples/pets/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jazz Rate My Pet Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/2_main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
examples/pets/job-template.nomad
Normal file
56
examples/pets/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
job "example-pets$BRANCH_SUFFIX" {
|
||||||
|
region = "global"
|
||||||
|
datacenters = ["*"]
|
||||||
|
|
||||||
|
group "static" {
|
||||||
|
count = 4
|
||||||
|
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
to = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
attribute = "${node.class}"
|
||||||
|
operator = "="
|
||||||
|
value = "mesh"
|
||||||
|
}
|
||||||
|
|
||||||
|
spread {
|
||||||
|
attribute = "${node.datacenter}"
|
||||||
|
weight = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
distinct_hosts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
task "server" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "$DOCKER_TAG"
|
||||||
|
ports = ["http"]
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
username = "$DOCKER_USER"
|
||||||
|
password = "$DOCKER_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service {
|
||||||
|
tags = ["public"]
|
||||||
|
name = "example-pets$BRANCH_SUFFIX"
|
||||||
|
port = "http"
|
||||||
|
provider = "consul"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
cpu = 50 # MHz
|
||||||
|
memory = 50 # MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# deploy bump 4
|
||||||
47
examples/pets/package.json
Normal file
47
examples/pets/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "jazz-example-pets",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.63",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
|
"@types/qrcode": "^1.5.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"jazz-browser-media-images": "^0.5.0",
|
||||||
|
"jazz-react": "^0.5.0",
|
||||||
|
"jazz-react-auth-local": "^0.4.16",
|
||||||
|
"lucide-react": "^0.274.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router": "^6.16.0",
|
||||||
|
"react-router-dom": "^6.16.0",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uniqolor": "^1.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
examples/pets/postcss.config.js
Normal file
6
examples/pets/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
examples/pets/public/jazz-logo.png
Normal file
BIN
examples/pets/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
52
examples/pets/src/1_types.ts
Normal file
52
examples/pets/src/1_types.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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 */
|
||||||
115
examples/pets/src/2_main.tsx
Normal file
115
examples/pets/src/2_main.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React from "react";
|
||||||
|
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 {
|
||||||
|
Button,
|
||||||
|
ThemeProvider,
|
||||||
|
TitleAndLogo,
|
||||||
|
} from "./basicComponents/index.ts";
|
||||||
|
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";
|
||||||
|
|
||||||
|
/** Walkthrough: The top-level provider `<WithJazz/>`
|
||||||
|
*
|
||||||
|
* This shows how to use the top-level provider `<WithJazz/>`,
|
||||||
|
* which provides the rest of the app with a `LocalNode` (used through `useJazz` later),
|
||||||
|
* based on `LocalAuth` that uses PassKeys (aka WebAuthn) to store a user's account secret
|
||||||
|
* - no backend needed. */
|
||||||
|
|
||||||
|
const appName = "Jazz Rate My Pet Example";
|
||||||
|
|
||||||
|
const auth = LocalAuth({
|
||||||
|
appName,
|
||||||
|
Component: PrettyAuthUI,
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Walkthrough: Creating pet posts & routing in `<App/>`
|
||||||
|
*
|
||||||
|
* <App> is the main app component, handling client-side routing based
|
||||||
|
* on the CoValue ID (CoID) of our PetPost, stored in the URL hash
|
||||||
|
* - which can also contain invite links.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { logOut } = useJazz();
|
||||||
|
|
||||||
|
const router = createHashRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <PostOverview />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/new",
|
||||||
|
element: <NewPetPostForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/pet/:petPostId",
|
||||||
|
element: <RatePetPostUI />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/invite/*",
|
||||||
|
element: <p>Accepting invite...</p>,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
useAcceptInvite((petPostID) => router.navigate("/pet/" + petPostID));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => router.navigate("/").then(logOut)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostOverview() {
|
||||||
|
const { me } = useJazz<Profile, PetAccountRoot>();
|
||||||
|
|
||||||
|
const myPosts = me.root?.posts;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{myPosts?.length ? (
|
||||||
|
<>
|
||||||
|
<h1>My posts</h1>
|
||||||
|
{myPosts.map(
|
||||||
|
(post) =>
|
||||||
|
post && (
|
||||||
|
<Link key={post.id} to={"/pet/" + post.id}>
|
||||||
|
{post.name}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
<Link to="/new">New post</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
examples/pets/src/3_NewPetPostForm.tsx
Normal file
107
examples/pets/src/3_NewPetPostForm.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { ChangeEvent, useCallback, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
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 { Input, Button } from "./basicComponents";
|
||||||
|
|
||||||
|
/** Walkthrough: TODO
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PartialPetPost = CoMap<{
|
||||||
|
name: string;
|
||||||
|
image?: Media.ImageDefinition["id"];
|
||||||
|
reactions: PetReactions["id"];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function NewPetPostForm() {
|
||||||
|
const { me } = useJazz<Profile, PetAccountRoot>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [newPostId, setNewPostId] = useState<
|
||||||
|
CoID<PartialPetPost> | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const newPetPost = useAutoSub(newPostId);
|
||||||
|
|
||||||
|
const onChangeName = useCallback(
|
||||||
|
(name: string) => {
|
||||||
|
if (newPetPost) {
|
||||||
|
newPetPost.set({ name });
|
||||||
|
} else {
|
||||||
|
const petPostGroup = me.createGroup();
|
||||||
|
const petPost = petPostGroup.createMap<PartialPetPost>({
|
||||||
|
name,
|
||||||
|
reactions: petPostGroup.createStream<PetReactions>().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewPostId(petPost.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[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
|
||||||
|
);
|
||||||
|
|
||||||
|
newPetPost.set({ image: image.id });
|
||||||
|
},
|
||||||
|
[newPetPost]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(() => {
|
||||||
|
if (!newPetPost) return;
|
||||||
|
const myPosts = me.root?.posts;
|
||||||
|
|
||||||
|
if (!myPosts) {
|
||||||
|
throw new Error("No posts list found");
|
||||||
|
}
|
||||||
|
|
||||||
|
myPosts.append(newPetPost.id as PetPost["id"]);
|
||||||
|
|
||||||
|
navigate("/pet/" + newPetPost.id);
|
||||||
|
}, [me.root?.posts, newPetPost, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<p>Share your pet with friends!</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Pet Name"
|
||||||
|
className="text-3xl py-6"
|
||||||
|
onChange={(event) => onChangeName(event.target.value)}
|
||||||
|
value={newPetPost?.name || ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{newPetPost?.image ? (
|
||||||
|
<img
|
||||||
|
className="w-80 max-w-full rounded"
|
||||||
|
src={
|
||||||
|
newPetPost?.image.as(BrowserImage)
|
||||||
|
?.highestResSrcOrPlaceholder
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
disabled={!newPetPost?.name}
|
||||||
|
onChange={onImageSelected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{newPetPost?.name && newPetPost?.image && (
|
||||||
|
<Button onClick={onSubmit}>Submit Post</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
examples/pets/src/4_RatePetPostUI.tsx
Normal file
126
examples/pets/src/4_RatePetPostUI.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { useParams } from "react-router";
|
||||||
|
import { CoID } from "cojson";
|
||||||
|
|
||||||
|
import { PetPost, ReactionType, REACTION_TYPES, PetReactions } from "./1_types";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/** Walkthrough: TODO
|
||||||
|
*/
|
||||||
|
|
||||||
|
const reactionEmojiMap: { [reaction in ReactionType]: string } = {
|
||||||
|
aww: "😍",
|
||||||
|
love: "❤️",
|
||||||
|
haha: "😂",
|
||||||
|
wow: "😮",
|
||||||
|
tiny: "🐥",
|
||||||
|
chonkers: "🐘",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RatePetPostUI() {
|
||||||
|
const petPostID = useParams<{ petPostId: CoID<PetPost> }>().petPostId;
|
||||||
|
|
||||||
|
const petPost = useAutoSub(petPostID);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">{petPost?.name}</h1>
|
||||||
|
<ShareButton petPost={petPost} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{petPost?.image && (
|
||||||
|
<img
|
||||||
|
className="w-80 max-w-full rounded"
|
||||||
|
src={
|
||||||
|
petPost.image.as(BrowserImage)
|
||||||
|
?.highestResSrcOrPlaceholder
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between max-w-xs flex-wrap">
|
||||||
|
{REACTION_TYPES.map((reactionType) => (
|
||||||
|
<Button
|
||||||
|
key={reactionType}
|
||||||
|
variant={
|
||||||
|
petPost?.reactions?.me?.last === reactionType
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
petPost?.reactions?.push(reactionType);
|
||||||
|
}}
|
||||||
|
title={`React with ${reactionType}`}
|
||||||
|
className="text-2xl px-2"
|
||||||
|
>
|
||||||
|
{reactionEmojiMap[reactionType]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{petPost?.meta.group.myRole() === "admin" && petPost.reactions && (
|
||||||
|
<ReactionOverview petReactions={petPost.reactions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReactionOverview({
|
||||||
|
petReactions,
|
||||||
|
}: {
|
||||||
|
petReactions: Resolved<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);
|
||||||
|
|
||||||
|
if (reactionsOfThisType.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
key={reactionType}
|
||||||
|
>
|
||||||
|
{reactionEmojiMap[reactionType]}{" "}
|
||||||
|
{reactionsOfThisType.map((reaction, idx) =>
|
||||||
|
reaction.by?.profile?.name ? (
|
||||||
|
<span
|
||||||
|
className="rounded-full py-0.5 px-2 text-xs"
|
||||||
|
style={uniqueColoring(reaction.by.id)}
|
||||||
|
key={reaction.by.id}
|
||||||
|
>
|
||||||
|
{reaction.by.profile.name}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Skeleton
|
||||||
|
className="mt-1 w-[50px] h-[1em] rounded-full"
|
||||||
|
key={idx}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueColoring(seed: string) {
|
||||||
|
const darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: uniqolor(seed, { lightness: darkMode ? 80 : 20 }).color,
|
||||||
|
background: uniqolor(seed, { lightness: darkMode ? 20 : 80 }).color,
|
||||||
|
};
|
||||||
|
}
|
||||||
12
examples/pets/src/basicComponents/TitleAndLogo.tsx
Normal file
12
examples/pets/src/basicComponents/TitleAndLogo.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Toaster } from ".";
|
||||||
|
|
||||||
|
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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
examples/pets/src/basicComponents/index.ts
Normal file
7
examples/pets/src/basicComponents/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { Button } from "./ui/button";
|
||||||
|
export { Input } from "./ui/input";
|
||||||
|
export { Toaster } from "./ui/toaster";
|
||||||
|
export { useToast } from "./ui/use-toast";
|
||||||
|
export { Skeleton } from "./ui/skeleton";
|
||||||
|
export { TitleAndLogo } from "./TitleAndLogo";
|
||||||
|
export { ThemeProvider } from "./themeProvider";
|
||||||
6
examples/pets/src/basicComponents/lib/utils.ts
Normal file
6
examples/pets/src/basicComponents/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
72
examples/pets/src/basicComponents/themeProvider.tsx
Normal file
72
examples/pets/src/basicComponents/themeProvider.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
defaultTheme?: string;
|
||||||
|
storageKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: string;
|
||||||
|
setTheme: (theme: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
theme: "system",
|
||||||
|
setTheme: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState(
|
||||||
|
() => localStorage.getItem(storageKey) || defaultTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)"
|
||||||
|
).matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
|
||||||
|
root.classList.add(systemTheme);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: string) => {
|
||||||
|
localStorage.setItem(storageKey, theme);
|
||||||
|
setTheme(theme);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext);
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
56
examples/pets/src/basicComponents/ui/button.tsx
Normal file
56
examples/pets/src/basicComponents/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
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"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
25
examples/pets/src/basicComponents/ui/input.tsx
Normal file
25
examples/pets/src/basicComponents/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
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"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
15
examples/pets/src/basicComponents/ui/skeleton.tsx
Normal file
15
examples/pets/src/basicComponents/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
127
examples/pets/src/basicComponents/ui/toast.tsx
Normal file
127
examples/pets/src/basicComponents/ui/toast.tsx
Normal file
@@ -0,0 +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 { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
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
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
33
examples/pets/src/basicComponents/ui/toaster.tsx
Normal file
33
examples/pets/src/basicComponents/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/basicComponents/ui/toast"
|
||||||
|
import { useToast } from "@/basicComponents/ui/use-toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
192
examples/pets/src/basicComponents/ui/use-toast.ts
Normal file
192
examples/pets/src/basicComponents/ui/use-toast.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/basicComponents/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
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
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_VALUE
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// ! 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.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
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])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
48
examples/pets/src/components/Auth.tsx
Normal file
48
examples/pets/src/components/Auth.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { LocalAuthComponent } from "jazz-react-auth-local";
|
||||||
|
|
||||||
|
import { Input, Button } from "../basicComponents";
|
||||||
|
|
||||||
|
export const PrettyAuthUI: LocalAuthComponent = ({
|
||||||
|
loading,
|
||||||
|
logIn,
|
||||||
|
signUp,
|
||||||
|
}) => {
|
||||||
|
const [username, setUsername] = useState<string>("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center p-5">
|
||||||
|
{loading ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-72 flex flex-col gap-4">
|
||||||
|
<form
|
||||||
|
className="w-72 flex flex-col gap-2"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
signUp(username);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Display name"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
autoComplete="webauthn"
|
||||||
|
className="text-base"
|
||||||
|
/>
|
||||||
|
<Button asChild>
|
||||||
|
<Input
|
||||||
|
type="submit"
|
||||||
|
value="Sign Up as new account"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<Button onClick={logIn}>
|
||||||
|
Log In with existing account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
examples/pets/src/components/ShareButton.tsx
Normal file
46
examples/pets/src/components/ShareButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { PetPost } from "../1_types";
|
||||||
|
|
||||||
|
import { Resolved, createInviteLink } from "jazz-react";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import { useToast, Button } from "../basicComponents";
|
||||||
|
|
||||||
|
export function ShareButton({ petPost }: { petPost?: Resolved<PetPost> }) {
|
||||||
|
const [existingInviteLink, setExistingInviteLink] = useState<string>();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
petPost?.meta.group.myRole() === "admin" && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="py-0"
|
||||||
|
disabled={!petPost}
|
||||||
|
variant="outline"
|
||||||
|
onClick={async () => {
|
||||||
|
let inviteLink = existingInviteLink;
|
||||||
|
if (petPost && !inviteLink) {
|
||||||
|
inviteLink = createInviteLink(petPost, "writer");
|
||||||
|
setExistingInviteLink(inviteLink);
|
||||||
|
}
|
||||||
|
if (inviteLink) {
|
||||||
|
const qr = await QRCode.toDataURL(inviteLink, {
|
||||||
|
errorCorrectionLevel: "L",
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Copied invite link to clipboard!",
|
||||||
|
description: (
|
||||||
|
<img src={qr} className="w-20 h-20" />
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
76
examples/pets/src/index.css
Normal file
76
examples/pets/src/index.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--primary: 24 9.8% 10%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--primary: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 24 5.7% 82.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
examples/pets/src/vite-env.d.ts
vendored
Normal file
1
examples/pets/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
76
examples/pets/tailwind.config.js
Normal file
76
examples/pets/tailwind.config.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}
|
||||||
29
examples/pets/tsconfig.json
Normal file
29
examples/pets/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/pets/tsconfig.node.json
Normal file
10
examples/pets/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
16
examples/pets/vite.config.ts
Normal file
16
examples/pets/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
minify: false
|
||||||
|
}
|
||||||
|
})
|
||||||
9
examples/todo/CHANGELOG.md
Normal file
9
examples/todo/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# jazz-example-todo
|
||||||
|
|
||||||
|
## 0.0.63
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- jazz-react@0.5.0
|
||||||
|
- jazz-react-auth-local@0.4.16
|
||||||
@@ -1,27 +1,64 @@
|
|||||||
# React + TypeScript + Vite
|
# Jazz Todo List Example
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
Live version: https://example-todo.jazz.tools
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
## Installing & running the example locally
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
Start by checking out just the example app to a folder:
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
```bash
|
||||||
|
npx degit gardencmp/jazz/examples/todo jazz-example-todo
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
cd jazz-example-todo
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
(This ensures that you have the example app without git history or our multi-package monorepo)
|
||||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
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).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"cssVariables": true
|
"cssVariables": true
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/basicComponents",
|
||||||
"utils": "@/lib/utils"
|
"utils": "@/basicComponents/lib/utils"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Jazz Todo List Example</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/2_main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
job "example-todo$BRANCH_SUFFIX" {
|
job "example-todo$BRANCH_SUFFIX" {
|
||||||
region = "$REGION"
|
region = "global"
|
||||||
datacenters = ["$REGION"]
|
datacenters = ["*"]
|
||||||
|
|
||||||
group "static" {
|
group "static" {
|
||||||
// count = 3
|
count = 4
|
||||||
|
|
||||||
network {
|
network {
|
||||||
port "http" {
|
port "http" {
|
||||||
@@ -14,13 +14,17 @@ job "example-todo$BRANCH_SUFFIX" {
|
|||||||
constraint {
|
constraint {
|
||||||
attribute = "${node.class}"
|
attribute = "${node.class}"
|
||||||
operator = "="
|
operator = "="
|
||||||
value = "edge"
|
value = "mesh"
|
||||||
}
|
}
|
||||||
|
|
||||||
// spread {
|
spread {
|
||||||
// attribute = "${node.datacenter}"
|
attribute = "${node.datacenter}"
|
||||||
// weight = 100
|
weight = 100
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
distinct_hosts = true
|
||||||
|
}
|
||||||
|
|
||||||
task "server" {
|
task "server" {
|
||||||
driver = "docker"
|
driver = "docker"
|
||||||
@@ -37,9 +41,7 @@ job "example-todo$BRANCH_SUFFIX" {
|
|||||||
|
|
||||||
service {
|
service {
|
||||||
tags = ["public"]
|
tags = ["public"]
|
||||||
meta {
|
name = "example-todo$BRANCH_SUFFIX"
|
||||||
public_name = "${BRANCH_SUBDOMAIN}example-todo"
|
|
||||||
}
|
|
||||||
port = "http"
|
port = "http"
|
||||||
provider = "consul"
|
provider = "consul"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-example-todo",
|
"name": "jazz-example-todo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.7",
|
"version": "0.0.63",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -12,15 +12,21 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
|
"@types/qrcode": "^1.5.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"jazz-react": "^0.0.13",
|
"jazz-react": "^0.5.0",
|
||||||
"jazz-react-auth-local": "^0.0.10",
|
"jazz-react-auth-local": "^0.4.16",
|
||||||
"lucide-react": "^0.265.0",
|
"lucide-react": "^0.274.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router": "^6.16.0",
|
||||||
|
"react-router-dom": "^6.16.0",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss-animate": "^1.0.6"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uniqolor": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
|
|||||||
BIN
examples/todo/public/jazz-logo.png
Normal file
BIN
examples/todo/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user