Compare commits

...

54 Commits

Author SHA1 Message Date
Anselm
46fcbd6c01 Implement first version of jazz-nodejs 2023-10-25 14:27:55 +01:00
Anselm
aa3e3de09e Update docs 2023-10-20 11:09:19 +01:00
Anselm
af3d48764d Reset allTweets 2023-10-20 10:50:29 +01:00
Anselm
091f36b736 Update all tweets 2023-10-20 10:38:56 +01:00
Anselm
7107f79f42 Update all tweets 2023-10-20 10:32:30 +01:00
Anselm
9922db2336 Cosmetic fixes 2023-10-20 10:23:40 +01:00
Anselm
75db570198 Use DemoAuth in Twit example 2023-10-20 10:18:14 +01:00
Anselm
28a09f377b Fix weird TypeScript error 2023-10-20 10:11:31 +01:00
Anselm
fd2e0855bb Deploy twit and chat 2023-10-20 09:56:09 +01:00
Anselm
82e1d57bd6 Fix new accounts not synced 2023-10-20 09:51:12 +01:00
Anselm
a2fbb0b0c8 new allTweets list 2023-10-19 13:38:55 +01:00
Anselm
8feddf9932 Fix sqlite type dependency 2023-10-19 13:38:43 +01:00
Anselm Eickhoff
feed34b1cf Merge pull request #122 from gardencmp/react-advanced
Performance improvements & Twit example improvements
2023-10-19 10:57:37 +01:00
Anselm
662c980cf2 First changeset 2023-10-19 00:52:47 +01:00
Anselm
f5ae530890 Add and use mapDefered to ResolvedCoList 2023-10-19 00:38:35 +01:00
Anselm
46bf7dd3ce A ton of performance and twit example improvements 2023-10-18 23:16:39 +01:00
Anselm
5d4eb38204 A bunch of perf improvements and sync fixes 2023-10-18 00:37:41 +01:00
Anselm
66da658075 Twit example improvements & initial stress test 2023-10-17 21:22:04 +01:00
Anselm
3477b74573 Lots of sync improvements, basic peer priority 2023-10-17 21:21:39 +01:00
Anselm
f3de4906b7 Prepare stress test and fix #83 2023-10-17 16:34:17 +01:00
Anselm
caded3f189 Fix unknown signer bug for incoming transactions 2023-10-17 14:39:13 +01:00
Anselm
5196395495 Wording 2023-10-17 12:03:48 +01:00
Anselm
8089a7ed9f Add report to parent frame again 2023-10-17 11:51:41 +01:00
Anselm
99230d31d2 Add plausible script 2023-10-17 11:49:30 +01:00
Anselm Eickhoff
94bca03f59 Merge pull request #121 from gardencmp/new-hp
New homepage PR 2
2023-10-17 11:39:04 +01:00
Anselm
49719b6e6d Fix example deploy 2023-10-17 11:28:10 +01:00
Anselm
1bdb781452 Fix iframe and metadata 2023-10-17 11:19:00 +01:00
Anselm
c336f69a6b Build and deploy homepage 2023-10-17 11:03:59 +01:00
Anselm
c8cb1ce208 Rename font 2023-10-17 09:55:45 +01:00
Anselm
814a6a80cd Lots of homepage improvements 2023-10-16 23:47:51 +01:00
Anselm Eickhoff
5fdfe18b32 Merge pull request #119 from gardencmp/new-hp
Chat demo & start of new homepage
2023-10-13 11:44:40 +01:00
Anselm
7b7a74778b Reduce number of chat example deployments 2023-10-13 11:40:36 +01:00
Anselm
39dbd46556 Publish
- jazz-example-chat@0.0.45
 - jazz-example-file-drop@0.0.62
 - jazz-example-pets@0.0.62
 - jazz-example-todo@0.0.62
 - jazz-example-twit@0.0.62
 - hash-slash@0.1.3
 - jazz-browser@0.4.15
 - jazz-browser-auth-local@0.4.15
 - jazz-browser-media-images@0.4.15
 - jazz-react@0.4.15
 - jazz-react-auth-local@0.4.15
2023-10-13 11:36:13 +01:00
Anselm
1db4a14be4 Update docs 2023-10-13 11:35:53 +01:00
Anselm
4a4ea4e196 Fix issues with packages 2023-10-13 11:35:35 +01:00
Anselm
e0724441eb Deploy chat example 2023-10-13 11:28:08 +01:00
Anselm
5d47895515 Rename hashroute to hash-slash 2023-10-13 11:25:46 +01:00
Anselm
c1dfac7260 Publish
- jazz-example-chat@0.0.44
 - jazz-example-file-drop@0.0.61
 - jazz-example-pets@0.0.61
 - jazz-example-todo@0.0.61
 - jazz-example-twit@0.0.61
 - hashroute@0.1.2
 - jazz-browser@0.4.14
 - jazz-browser-auth-local@0.4.14
 - jazz-browser-media-images@0.4.14
 - jazz-react@0.4.14
 - jazz-react-auth-local@0.4.14
2023-10-13 11:20:40 +01:00
Anselm
bf29cb3bae Make stuff mergeable 2023-10-13 11:20:07 +01:00
Anselm
a0a9b3f851 Merge branch 'main' into new-hp 2023-10-13 11:15:24 +01:00
Anselm Eickhoff
46330ae201 Merge pull request #117 from gardencmp:data-throughput
Improve data throughput of BinaryCoStreams
2023-10-09 11:26:56 +01:00
Anselm
bfe3595b4c Fix errors in file-drop example 2023-10-09 11:23:15 +01:00
Anselm
34c39e6a55 Scale down other examples and add file-drop 2023-10-09 11:12:17 +01:00
Anselm
5a85501919 Publish
- jazz-example-file-drop@0.0.60
 - jazz-example-pets@0.0.60
 - jazz-example-todo@0.0.60
 - jazz-example-twit@0.0.60
 - cojson@0.4.13
 - cojson-simple-sync@0.4.13
 - cojson-storage-indexeddb@0.4.13
 - cojson-storage-sqlite@0.4.13
 - jazz-autosub@0.4.13
 - jazz-browser@0.4.13
 - jazz-browser-auth-local@0.4.13
 - jazz-browser-media-images@0.4.13
 - jazz-react@0.4.13
 - jazz-react-auth-local@0.4.13
2023-10-06 17:31:14 +01:00
Anselm
97a4282e5e Merge branch 'backend-support' into data-throughput 2023-10-06 17:23:13 +01:00
Anselm
39c13b50a3 Update docs 2023-10-06 17:18:30 +01:00
Anselm
ad304e321b Lots of improvements for BinaryCoStreams & file-drop example 2023-10-06 17:09:58 +01:00
Anselm
72fce45b2b Publish
- jazz-example-pets@0.0.21
 - jazz-example-todo@0.0.45
 - jazz-example-twit@0.0.8
 - cojson@0.4.8
 - cojson-simple-sync@0.4.8
 - cojson-storage-indexeddb@0.4.8
 - cojson-storage-sqlite@0.4.8
 - jazz-autosub@0.4.8
 - jazz-browser@0.4.8
 - jazz-browser-auth-local@0.4.8
 - jazz-browser-media-images@0.4.9
 - jazz-react@0.4.8
 - jazz-react-auth-local@0.4.8
2023-10-04 22:01:28 +01:00
Anselm
1f49d7fda6 Actually fix circular issues for esbuild/vite 2023-10-04 22:00:38 +01:00
Anselm
eec8ee7027 Publish
- jazz-example-pets@0.0.20
 - jazz-example-todo@0.0.44
 - jazz-example-twit@0.0.7
 - cojson@0.4.7
 - cojson-simple-sync@0.4.7
 - cojson-storage-indexeddb@0.4.7
 - cojson-storage-sqlite@0.4.7
 - jazz-autosub@0.4.7
 - jazz-browser@0.4.7
 - jazz-browser-auth-local@0.4.7
 - jazz-browser-media-images@0.4.8
 - jazz-react@0.4.7
 - jazz-react-auth-local@0.4.7
2023-10-04 21:23:52 +01:00
Anselm
188eb2e1e3 Update docs 2023-10-04 21:23:28 +01:00
Anselm
62867b32d9 Get rid of more cyclic imports 2023-10-04 21:22:52 +01:00
Anselm
ccebd2447d Publish
- jazz-example-pets@0.0.19
 - jazz-example-todo@0.0.43
 - jazz-example-twit@0.0.6
 - cojson@0.4.6
 - cojson-simple-sync@0.4.6
 - cojson-storage-indexeddb@0.4.6
 - cojson-storage-sqlite@0.4.6
 - jazz-autosub@0.4.6
 - jazz-browser@0.4.6
 - jazz-browser-auth-local@0.4.6
 - jazz-browser-media-images@0.4.7
 - jazz-react@0.4.6
 - jazz-react-auth-local@0.4.6
2023-10-04 21:01:44 +01:00
Anselm
08dca75789 Address some circular deps in cojson typescript 2023-10-04 21:00:59 +01:00
191 changed files with 11888 additions and 7080 deletions

8
.changeset/README.md Normal file
View 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
View 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": []
}

View File

@@ -7,11 +7,12 @@ on:
branches: [ "main" ]
jobs:
build:
build-examples:
runs-on: ubuntu-latest
strategy:
matrix:
example: ["todo", "pets", "twit"]
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
steps:
- uses: actions/checkout@v3
@@ -53,12 +54,40 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
# 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
needs: build-examples
strategy:
matrix:
example: ["todo", "pets", "twit"]
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
steps:
- uses: actions/checkout@v3
@@ -87,4 +116,37 @@ jobs:
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
working-directory: ./examples/${{ matrix.example }}
working-directory: ./examples/${{ matrix.example }}
# deploy-homepage:
# runs-on: ubuntu-latest
# needs: build-homepage
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
# - uses: gacts/install-nomad@v1
# - name: Tailscale
# uses: tailscale/github-action@v1
# with:
# authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
# - name: Deploy on Nomad
# run: |
# if [ "${{github.ref_name}}" == "main" ]; then
# export BRANCH_SUFFIX="";
# export BRANCH_SUBDOMAIN="";
# else
# export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
# export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
# fi
# export DOCKER_USER=gardencmp;
# export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
# export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
# envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
# cat job-instance.nomad;
# NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
# working-directory: ./homepage/homepage-jazz

457
DOCS.md

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -3,7 +3,7 @@ job "chat$BRANCH_SUFFIX" {
datacenters = ["*"]
group "static" {
count = 8
count = 4
network {
port "http" {

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.43",
"version": "0.0.46",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,9 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.4.6",
"jazz-react-auth-local": "^0.4.6",
"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",

View File

@@ -1,6 +1,6 @@
import { WithJazz, useJazz, DemoAuth } from 'jazz-react';
import ReactDOM from 'react-dom/client';
import { HashRoute } from 'hashroute';
import { HashRoute } from 'hash-slash';
import { ChatWindow } from './chatWindow.tsx';
import { Chat } from './dataModel.ts';
@@ -18,7 +18,7 @@ function App() {
{HashRoute({
'/': <Home />,
'/chat/:id': (id) => <ChatWindow chatId={id as Chat['id']} />,
})}
}, { reportToParentFrame: true })}
</div>
}

View 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
View 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?

View 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

View 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/

View 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).

View 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"
}
}

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

View 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

View 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"
}
}

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,5 @@
import { CoMap, BinaryCoStream } from "cojson";
export type FileBundle = CoMap<{
[filename: string]: BinaryCoStream['id']
}>;

View 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 */

View File

@@ -0,0 +1,39 @@
import { Input } from "@/basicComponents/ui/input";
import { Button } from "@/basicComponents/ui/button";
export function SubmittableInput({
onSubmit,
label,
placeholder,
disabled,
}: {
onSubmit: (text: string) => void;
label: string;
placeholder: string;
disabled?: boolean;
}) {
return (
<form
className="flex flex-row items-center gap-3"
onSubmit={(e) => {
e.preventDefault();
const textEl = e.currentTarget.elements.namedItem(
"text"
) as HTMLInputElement;
onSubmit(textEl.value);
textEl.value = "";
}}
>
<Input
className="-ml-3 -my-2 flex-grow flex-3 text-base"
name="text"
placeholder={placeholder}
autoComplete="off"
disabled={disabled}
/>
<Button asChild type="submit" className="flex-shrink flex-1 cursor-pointer">
<Input type="submit" value={label} disabled={disabled} />
</Button>
</form>
);
}

View 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 />
</>
}

View 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";

View 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))
}

View 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;
};

View 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 }

View File

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

View 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 }

View 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 }

View File

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

View 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,
}

View 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>
)
}

View 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 }

View 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>
);
};

View 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>
)
);
}

View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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")],
}

View 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" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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
}
})

View 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

View File

@@ -3,7 +3,7 @@ job "example-pets$BRANCH_SUFFIX" {
datacenters = ["*"]
group "static" {
count = 8
count = 4
network {
port "http" {

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.19",
"version": "0.0.63",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,9 +16,9 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "^0.4.7",
"jazz-react": "^0.4.6",
"jazz-react-auth-local": "^0.4.6",
"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",

View 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

View File

@@ -3,7 +3,7 @@ job "example-todo$BRANCH_SUFFIX" {
datacenters = ["*"]
group "static" {
count = 8
count = 4
network {
port "http" {

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.43",
"version": "0.0.63",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.4.6",
"jazz-react-auth-local": "^0.4.6",
"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",

View File

@@ -24,10 +24,15 @@ export type TodoProject = CoMap<{
export type ListOfProjects = CoList<TodoProject["id"]>;
/** The account root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */
export type TodoAccountRoot = CoMap<{
projects: ListOfProjects["id"];
}>;
/** The account migration is run on account creation and on every log-in.
* You can use it to set up the account root and any other initial CoValues you need.
*/
export const migration: AccountMigration<Profile, TodoAccountRoot> = (account) => {
if (!account.get("root")) {
account.set(

View File

@@ -25,9 +25,11 @@ 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
* which provides the rest of the app with a controlled account (used through `useJazz` later).
* Here we use `LocalAuth`, which uses Passkeys (aka WebAuthn) to store a user's account secret
* - no backend needed.
*
* `<WithJazz/>` also runs our account migration
*/
const appName = "Jazz Todo List Example";

26
examples/twit-stresstest/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# 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?
.env

View File

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

View File

@@ -0,0 +1,104 @@
import { LocalNode, cojsonReady, ControlledAccount, AccountID } from "cojson";
import {
ALL_TWEETS_LIST_ID,
LikeStream,
ListOfTwits,
ReplyStream,
Twit,
TwitAccountRoot,
TwitProfile,
migration,
} from "../twit/src/1_dataModel.js";
import { createOrResumeWorker, autoSub } from "jazz-nodejs"
async function runner() {
const { localNode: node, worker } = await createOrResumeWorker(
"TwitStressTestBot" + Math.random().toString(36).slice(2),
);
console.log(
"profile",
node.expectProfileLoaded(node.account.id as AccountID).id
);
await new Promise((resolve) => setTimeout(resolve, 10_000));
const loadedAllTwits = await node.load(ALL_TWEETS_LIST_ID);
if (loadedAllTwits === "unavailable") {
throw new Error("allTweets is unavailable");
}
let allTwits = loadedAllTwits;
let startedPosting = false;
autoSub(
(node.account as ControlledAccount<TwitProfile, TwitAccountRoot>).id,
node,
async (me) => {
if (
!me?.root?.peopleWhoCanSeeMyContent ||
!me.root.peopleWhoCanInteractWithMe
)
return;
if (startedPosting) return;
startedPosting = true;
for (let i = 0; i < 10; i++) {
await new Promise((resolve) =>
setTimeout(resolve, Math.random() * 120000)
// setTimeout(resolve, Math.random() * 5000)
);
const audience = me.root.peopleWhoCanSeeMyContent;
const interactors = me.root.peopleWhoCanInteractWithMe;
if (!audience || !interactors) return;
console.log("Posting twit ", i);
const twit = audience.createMap<Twit>({
text: "Hello world " + i,
likes: interactors.createStream<LikeStream>().id,
replies: interactors.createStream<ReplyStream>().id,
});
me.profile?.twits?.prepend(twit?.id as Twit["id"]);
allTwits = allTwits?.prepend(twit.id);
}
}
);
let blackHole = 0;
let lastUpdate = Date.now()
autoSub(ALL_TWEETS_LIST_ID, node, (allTwits) => {
if (Date.now() - lastUpdate < 33) return;
lastUpdate = Date.now();
// console.log("All twits updated", new Date());
// console.log(allTwits
// ?.slice(0, 20)
// .map(
// (twit) =>
// twit?.text +
// "/" +
// twit?.meta.edits.text?.by?.profile?.name
// )
// .length, allTwits?.length);
blackHole +=
allTwits
?.slice(0, 20)
.map(
(twit) =>
twit?.text +
"/" +
twit?.meta.edits.text?.by?.profile?.name
).length || 0;
});
}
for (let i = 0; i < 50; i++) {
runner();
}

View File

@@ -0,0 +1,17 @@
import { ControlledAccount, LocalNode, cojsonReady } from "cojson";
import {
ListOfTwits,
migration,
} from "../twit/src/1_dataModel";
import { createOrResumeWorker, autoSub } from "jazz-nodejs"
const { localNode: node, worker } = await createOrResumeWorker(
"TwitAllTwitsCreator"
);
const allTweetsGroup = worker.createGroup();
allTweetsGroup.addMember('everyone', 'writer');
const allTweets = allTweetsGroup.createList<ListOfTwits>();
console.log("allTweets", allTweets.id);

View File

@@ -0,0 +1,17 @@
{
"name": "twit-stresstest",
"version": "0.1.0",
"main": "dist/twit-stresstest/index.js",
"type":"module",
"license": "MIT",
"private": true,
"dependencies": {
"jazz-nodejs": "^0.5.0"
},
"scripts": {
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist",
"stress4": "npx concurrently \"bun --inspect index.ts\" \"bun index.ts\" \"bun index.ts\" \"bun index.ts\"",
"stress8": "npx concurrently \"bun --inspect index.ts\" \"bun index.ts\" \"bun index.ts\" \"bun index.ts\" \"bun index.ts\" \"bun index.ts\" \"bun index.ts\" \"bun index.ts\"",
"stress8-built": "npx concurrently \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\" \"node dist/twit-stresstest/index.js\""
}
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
},
"include": ["./index.ts"],
}

View File

@@ -0,0 +1,14 @@
# jazz-example-twit
## 0.1.0
### Minor Changes
- Adding a lot of performance improvements to cojson, add a stresstest for the twit example and make that run smoother in a lot of ways.
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.5.0
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -3,7 +3,7 @@ job "twit$BRANCH_SUFFIX" {
datacenters = ["*"]
group "static" {
count = 8
count = 4
network {
port "http" {

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-twit",
"private": true,
"version": "0.0.6",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -18,13 +18,14 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"javascript-time-ago": "^2.5.9",
"jazz-browser-media-images": "^0.4.7",
"jazz-react": "^0.4.6",
"jazz-react-auth-local": "^0.4.6",
"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-intersection-observer": "^9.5.2",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-time-ago": "^7.2.1",

View File

@@ -29,36 +29,30 @@ export type TwitProfile = Profile<
>;
export type TwitAccountRoot = CoMap<{
peopleWhoCanSeeMyTwits: Group['id'];
peopleWhoCanSeeMyFollows: Group['id'];
peopleWhoCanFollowMe: Group['id'];
peopleWhoCanSeeMyContent: Group['id'];
peopleWhoCanInteractWithMe: Group['id'];
}>;
export const ALL_TWEETS_LIST_ID = "co_zQEhxDTvZt3f4vWKqVNj9TCTRs4" as ListOfTwits['id'];
export const migration: AccountMigration<TwitProfile, TwitAccountRoot> = (account, profile) => {
if (!account.get('root')) {
const peopleWhoCanSeeMyTwits = account.createGroup();
const peopleWhoCanSeeMyFollows = account.createGroup();
const peopleWhoCanFollowMe = account.createGroup();
const peopleWhoCanSeeMyContent = account.createGroup();
const peopleWhoCanInteractWithMe = account.createGroup();
peopleWhoCanFollowMe?.addMember(EVERYONE, 'writer');
peopleWhoCanSeeMyTwits?.addMember(EVERYONE, 'reader');
peopleWhoCanSeeMyFollows?.addMember(EVERYONE, 'reader');
peopleWhoCanSeeMyContent?.addMember(EVERYONE, 'reader');
peopleWhoCanInteractWithMe?.addMember(EVERYONE, 'writer');
const root = account.createMap<TwitAccountRoot>({
peopleWhoCanSeeMyTwits: peopleWhoCanSeeMyTwits.id,
peopleWhoCanSeeMyFollows: peopleWhoCanSeeMyFollows.id,
peopleWhoCanFollowMe: peopleWhoCanFollowMe.id,
peopleWhoCanSeeMyContent: peopleWhoCanSeeMyContent.id,
peopleWhoCanInteractWithMe: peopleWhoCanInteractWithMe.id
});
account.set('root', root.id);
profile.set('twits', peopleWhoCanSeeMyTwits.createList<ListOfTwits>().id, 'trusting');
profile.set('following', peopleWhoCanSeeMyFollows.createList<ListOfProfiles>().id, 'trusting');
profile.set('followers', peopleWhoCanFollowMe.createStream<StreamOfFollowers>().id, 'trusting');
profile.set('twits', peopleWhoCanSeeMyContent.createList<ListOfTwits>().id, 'trusting');
profile.set('following', peopleWhoCanSeeMyContent.createList<ListOfProfiles>().id, 'trusting');
profile.set('followers', peopleWhoCanInteractWithMe.createStream<StreamOfFollowers>().id, 'trusting');
console.log('MIGRATION SUCCESSFUL!');
}
};

View File

@@ -1,37 +1,33 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider, createHashRouter } from 'react-router-dom';
import './index.css';
import { AccountMigration } from 'cojson';
import { WithJazz, useJazz } from 'jazz-react';
import { LocalAuth } from 'jazz-react-auth-local';
import { DemoAuth, WithJazz, useJazz } from 'jazz-react';
import { Button, ThemeProvider, TitleAndLogo } from './basicComponents/index.tsx';
import { PrettyAuthUI } from './components/Auth.tsx';
import { migration } from './1_dataModel.ts';
import { ChronoFeed } from './3_ChronoFeed.tsx';
import { AllTwitsFeed, FollowingFeed } from './3_ChronoFeed.tsx';
import { ProfilePage } from './5_ProfilePage.tsx';
const appName = 'Jazz Twit Example';
const auth = LocalAuth({
appName,
Component: PrettyAuthUI
const auth = DemoAuth({
appName
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
// <React.StrictMode>
<WithJazz auth={auth} migration={migration as AccountMigration}>
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-stretch justify-start gap-10 pt-10 pb-10 px-5 w-full max-w-xl mx-auto">
<WithJazz auth={auth} migration={migration as AccountMigration}>
<App />
</WithJazz>
<App />
</div>
</ThemeProvider>
</React.StrictMode>
</WithJazz>
// </React.StrictMode>
);
function App() {
@@ -40,7 +36,11 @@ function App() {
const router = createHashRouter([
{
path: '/',
element: <ChronoFeed />
element: <AllTwitsFeed />
},
{
path: '/following',
element: <FollowingFeed />
},
{
path: '/:profileId',
@@ -58,6 +58,9 @@ function App() {
<Button onClick={() => router.navigate('/')} variant="link" className="-ml-3">
Home
</Button>
<Button onClick={() => router.navigate('/following')} variant="link" className="-ml-3">
Following
</Button>
<Button onClick={() => router.navigate('/me')} variant="link" className="ml-auto">
My Profile
</Button>

View File

@@ -1,11 +1,38 @@
import { useMemo } from 'react';
import { useJazz } from 'jazz-react';
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { useEffect, useMemo, useState } from 'react';
import { useAutoSub, useJazz } from 'jazz-react';
import { ALL_TWEETS_LIST_ID, TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
import { TwitComponent } from './4_TwitComponent.tsx';
import { MainH1 } from './basicComponents/index.tsx';
import { LazyLoadRow, MainH1 } from './basicComponents/index.tsx';
export function ChronoFeed() {
export function AllTwitsFeed() {
const allTwits = useAutoSub(ALL_TWEETS_LIST_ID);
const [animate, setAnimate] = useState(false);
useEffect(() => {
if (!animate && allTwits?.length) {
setTimeout(() => setAnimate(true), 1000);
}
}, [allTwits, animate])
return (
<div className="flex flex-col items-stretch">
<CreateTwitForm className="mb-10" />
<MainH1>
All {allTwits?.length} Twits{' '}
<span className="text-sm">
{allTwits?.mapDeferred(({ loaded }) => loaded).filter(l => l).length || 0} loaded
</span>
</MainH1>
{allTwits?.mapDeferred(twit => (
<LazyLoadRow key={twit.id} animate={animate}>{() => <TwitComponent twit={twit.value()} />}</LazyLoadRow>
))}
</div>
);
}
export function FollowingFeed() {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const myTwits = me.profile?.twits;

View File

@@ -13,6 +13,7 @@ import {
TwitHeader,
TwitBody,
TwitText,
Placeholder,
} from './basicComponents/index.tsx';
import { Twit, TwitProfile } from './1_dataModel.ts';
import { BrowserImage } from 'jazz-browser-media-images';
@@ -32,49 +33,53 @@ export function TwitComponent({
const posterProfile = twit?.meta.edits.text?.by?.profile as Resolved<TwitProfile> | undefined;
const isTopLevel = !twit?.isReplyTo || alreadyInReplies;
const loadReactions = !!posterProfile?.name;
return (
<TwitWithRepliesContainer isTopLevel={isTopLevel}>
<TwitContainer>
<ProfilePicImg
src={posterProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
linkTo={'/' + posterProfile?.id}
linkTo={posterProfile?.id && ('/' + posterProfile.id)}
initial={posterProfile?.name[0]}
size={twit?.isReplyTo && "sm"}
/>
<TwitBody>
<TwitHeader>
<Link to={'/' + posterProfile?.id} className="font-bold hover:underline">
{posterProfile?.name}
</Link>
{posterProfile ? <Link to={'/' + posterProfile.id} className="font-bold hover:underline">
{posterProfile.name}
</Link> : <Placeholder/>}
{/* <div className='ml-2 text-xs text-neutral-200 dark:text-neutral-800'>{twit?.id}</div> */}
<SubtleRelativeTimeAgo dateTime={twit?.meta.edits.text?.at} />
</TwitHeader>
<TwitText style={posterProfile?.twitStyle}>
{/* This is where the tweet text goes */}
{twit?.text}
{twit?.text || <Placeholder/>}
</TwitText>
{twit?.images && (
<TwitImgGallery>
{twit.images.map(image => (
<TwitImg src={image?.as(BrowserImage)?.highestResSrcOrPlaceholder} key={image?.id} />
{twit.images.map((image, idx) => (
<TwitImg src={image?.as(BrowserImage)?.highestResSrcOrPlaceholder} key={image?.id || idx} />
))}
</TwitImgGallery>
)}
<ReactionsContainer>
<ButtonWithCount
active={twit?.likes?.me?.last === '❤️'}
active={loadReactions && (twit?.likes?.me?.last === '❤️')}
onClick={() => twit?.likes?.push(twit?.likes?.me?.last ? null : '❤️')}
count={twit?.likes?.perAccount.filter(([, liked]) => liked.last === '❤️').length || 0}
count={loadReactions && (twit?.likes?.perAccount.filter(([, liked]) => liked.last === '❤️').length) || 0}
icon={<HeartIcon size="18" />}
activeIcon={<HeartIcon color="red" size="18" fill="red" />}
disabled={!loadReactions || !twit?.likes?.perAccount}
/>
<ButtonWithCount
onClick={() => setShowReplyForm(s => !s)}
count={twit?.replies?.perAccount.flatMap(([, byAccount]) => byAccount.all).length || 0}
count={loadReactions && (twit?.replies?.perAccount.flatMap(([, byAccount]) => byAccount.all).length) || 0}
icon={<MessagesSquareIcon size="18" />}
disabled={!loadReactions || !twit?.replies?.perAccount}
/>
</ReactionsContainer>
</TwitBody>
@@ -89,7 +94,7 @@ export function TwitComponent({
/>
)}
{twit?.replies?.perAccount
{loadReactions && twit?.replies?.perAccount
.flatMap(([, byAccount]) => byAccount.all)
.sort((a, b) => b.at.getTime() - a.at.getTime())
.map(replyEntry => (

View File

@@ -39,7 +39,7 @@ export function ProfilePage() {
? null
: twit?.isReplyTo
: twit
);
) || [];
}, [profile?.twits]);
const [qr, setQr] = useState<string>('');
@@ -64,8 +64,8 @@ export function ProfilePage() {
{isMe && (
<ChooseProfilePicInput
onChange={(file: File) =>
me.root?.peopleWhoCanSeeMyTwits &&
createImage(file, me.root.peopleWhoCanSeeMyTwits, 256).then(image => {
me.root?.peopleWhoCanSeeMyContent &&
createImage(file, me.root.peopleWhoCanSeeMyContent, 256).then(image => {
me.profile?.set({ avatar: image.id }, 'trusting');
})
}

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect } from 'react';
import { Resolved, useJazz } from 'jazz-react';
import { Resolved, useJazz, useSyncedValue } from 'jazz-react';
import { AddTwitPicsInput, TwitImg, TwitTextInput } from './basicComponents/index.tsx';
import { LikeStream, ListOfImages, ReplyStream, Twit, TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { ALL_TWEETS_LIST_ID, LikeStream, ListOfImages, ReplyStream, Twit, TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { createImage } from 'jazz-browser-media-images';
export function CreateTwitForm(
@@ -12,12 +12,13 @@ export function CreateTwitForm(
} = {}
) {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const allTwits = useSyncedValue(ALL_TWEETS_LIST_ID);
const [pics, setPics] = React.useState<File[]>([]);
const onSubmit = useCallback(
(twitText: string) => {
const audience = me.root?.peopleWhoCanSeeMyTwits;
const audience = me.root?.peopleWhoCanSeeMyContent;
const interactors = me.root?.peopleWhoCanInteractWithMe;
if (!audience || !interactors) return;
@@ -29,19 +30,25 @@ export function CreateTwitForm(
me.profile?.twits?.prepend(twit?.id as Twit['id']);
if (!props.inReplyTo) {
allTwits?.prepend(twit.id);
}
if (props.inReplyTo) {
props.inReplyTo.replies?.push(twit.id);
twit.set({ isReplyTo: props.inReplyTo.id });
}
Promise.all(pics.map(pic => createImage(pic, twit.group, 1024))).then(createdPics => {
twit.set({ images: audience.createList<ListOfImages>(createdPics.map(pic => pic.id)).id });
});
if (pics.length > 0) {
Promise.all(pics.map(pic => createImage(pic, twit.group, 1024))).then(createdPics => {
twit.set({ images: audience.createList<ListOfImages>(createdPics.map(pic => pic.id)).id });
});
}
setPics([]);
props.onSubmit?.();
},
[me.profile?.twits, me.root?.peopleWhoCanSeeMyTwits, me.root?.peopleWhoCanInteractWithMe, props, pics]
[me.profile?.twits, me.root?.peopleWhoCanSeeMyContent, me.root?.peopleWhoCanInteractWithMe, props, pics, allTwits]
);
const [picPreviews, setPicPreviews] = React.useState<string[]>([]);

View File

@@ -25,7 +25,7 @@ export function FollowButton({ profile }: { profile?: Resolved<TwitProfile> }) {
return profile?.id === me.profile?.id ? (
<div className="ml-auto text-neutral-500">That's you!</div>
) : (
<Button onClick={followOrUnfollow} className="ml-auto" variant={alreadyFollowing ? 'ghost' : 'default'}>
<Button onClick={followOrUnfollow} className="ml-auto" disabled={!profile?.followers || !me.profile?.following} variant={alreadyFollowing ? 'ghost' : 'default'}>
{alreadyFollowing ? 'Unfollow' : theyFollowMe ? 'Follow Back' : 'Follow'}
</Button>
);

View File

@@ -17,6 +17,8 @@ export { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import TimeAgo from 'javascript-time-ago';
import en from 'javascript-time-ago/locale/en.json';
import { useInView } from 'react-intersection-observer';
import { useEffect, useState } from 'react';
TimeAgo.addDefaultLocale(en);
export function BioInput(props: { value?: string; onChange: (value: string) => void }) {
@@ -53,7 +55,7 @@ export function ChooseProfilePicInput(props: { onChange: (file: File) => void })
Choose Pic
<Input
type="file"
accept="image/*"
accept="image/jpg,image/jpeg,image/png,image/gif"
onChange={e => {
e.target.files?.[0] && props.onChange(e.target.files[0]);
e.target.value = '';
@@ -72,14 +74,17 @@ export function ProfilePicImg(props: { src?: string; size?: 'sm' | 'xxl'; linkTo
<img
src={props.src}
className={
'bg-neutral-200 rounded-full mr-2 object-cover shrink-0' +
'bg-neutral-200 dark:bg-neutral-800 rounded-full mr-2 object-cover shrink-0' +
(props.size === 'sm' ? ' w-8 h-8' : props.size === 'xxl' ? ' w-20 h-20' : ' w-10 h-10')
}
/>
) : (
<div
className={
'bg-neutral-200 rounded-full mr-2 object-cover shrink-0 flex items-center justify-center text-neutral-700 ' +
'rounded-full mr-2 object-cover shrink-0 flex items-center justify-center text-neutral-700 dark:text-neutral-300 ' +
(props.initial
? 'bg-neutral-200 dark:bg-neutral-800 '
: 'animate-pulse bg-neutral-100 dark:bg-neutral-900 ') +
(props.size === 'sm'
? ' w-8 h-8 text-[1.5rem]'
: props.size === 'xxl'
@@ -97,13 +102,17 @@ export function ProfilePicImg(props: { src?: string; size?: 'sm' | 'xxl'; linkTo
export function SubtleRelativeTimeAgo(props: { dateTime?: Date }) {
return (
<div className="ml-auto text-neutral-300 text-xs whitespace-nowrap">
<ReactTimeAgo date={props.dateTime || 0} />
{props.dateTime ? <ReactTimeAgo date={props.dateTime} timeStyle="round"/> : <Placeholder />}
</div>
);
}
export function TwitImg(props: { src?: string }) {
return <img src={props.src} className="h-40 rounded object-cover" />;
return props.src ? (
<img src={props.src} className="h-40 rounded object-cover" />
) : (
<div className="h-40 w-30 rounded bg-neutral-100" />
);
}
export function ReactionsContainer(props: { children: React.ReactNode }) {
@@ -120,18 +129,20 @@ export function ButtonWithCount(props: {
active?: boolean;
icon: React.ReactNode;
activeIcon?: React.ReactNode;
disabled?: boolean;
}) {
return (
<div className="flex items-center">
<Button
className="w-10 h-7 p-1 mr-1"
className={"w-10 h-7 p-1 mr-1 " + (props.disabled ? "text-neutral-200 dark:text-neutral-800" : "")}
variant={props.active ? 'secondary' : 'outline'}
onClick={props.onClick}
size="icon"
disabled={props.disabled}
>
{props.active ? props.activeIcon : props.icon}
</Button>{' '}
<span className="tabular-nums">{props.count}</span>
<span className={"tabular-nums " + (props.disabled ? "text-neutral-200 dark:text-neutral-800" : "")}>{props.count}</span>
</div>
);
}
@@ -174,7 +185,7 @@ export function AddTwitPicsInput(props: { onChange: (files: File[]) => void }) {
props.onChange(Array.from(e.target.files || []));
}}
className="hidden"
accept="image/*"
accept="image/jpg,image/jpeg,image/png,image/gif"
multiple
/>
</label>
@@ -203,7 +214,7 @@ export function TwitHeader(props: { children: React.ReactNode }) {
}
export function TwitImgGallery(props: { children: React.ReactNode }) {
return <div className="flex gap-2 mt-2 max-w-full overflow-auto">{props.children}</div>;
return <div className="flex gap-2 mt-2 max-w-full overflow-auto">{props.children || <TwitImg />}</div>;
}
export function TwitText(props: { children: React.ReactNode; style?: React.CSSProperties }) {
@@ -215,14 +226,40 @@ export function QuoteContainer(props: { children: React.ReactNode }) {
}
export function MainH1(props: { children: React.ReactNode }) {
return <h1 className="text-2xl mb-4">{props.children}</h1>;
return <h1 className="text-2xl mb-4 sticky top-0 p-4 -mx-4 bg-background z-20">{props.children}</h1>;
}
export function SmallInlineButton(props: { children: React.ReactNode } & ButtonProps) {
const {children, ...rest} = props
const { children, ...rest } = props;
return (
<Button variant={'ghost'} className="h-6 px-1 -mx-1" {...rest}>
{children}
</Button>
);
}
export function Placeholder() {
return (
<span className="bg-neutral-100 dark:bg-neutral-900 rounded animate-pulse text-transparent">
Loading, loading...
</span>
);
}
export function LazyLoadRow(props: { children: () => React.ReactNode, animate?: boolean }) {
const { ref, inView } = useInView({
// triggerOnce: true,
delay: 100,
});
const [height, setHeight] = useState(props.animate ? "0": "500px");
useEffect(() => {
setHeight("500px")
},[])
return (
<div ref={ref} style={{
maxHeight: height,
overflowX: "scroll",
transition: 'max-height 1s ease-in-out',
}}>{inView ? props.children() : <div className="mb-[1px] h-28 bg-neutral-50 dark:bg-neutral-950" />}</div>
);
}

View File

@@ -1,47 +1,8 @@
import { readFile, writeFile } from "fs/promises";
import { genDocsMd } from "./genDocsMd";
export const manuallyIgnore = new Set(["CojsonInternalTypes"]);
async function main() {
const exampleFilesInDocs = {
"examples/chat/src/dataModel.ts": "homepage/homepage-jazz/pages/index.mdx",
"examples/chat/src/app.tsx": "homepage/homepage-jazz/pages/index.mdx",
"examples/chat/src/chatWindow.tsx": "homepage/homepage-jazz/pages/index.mdx",
};
for (const [src, dest] of Object.entries(exampleFilesInDocs)) {
const srcStr = await readFile(src, "utf8");
const destStr = await readFile(dest, "utf8");
const srcFilename = src.split("/").pop()!;
const regexp = new RegExp(
'```(\\w+?) filename="' + srcFilename + '"(.+?)\n(.+?)```',
"s"
);
console.log(regexp);
await writeFile(
dest,
destStr.replace(regexp, (match, filetype, attrs, _oldCode) => {
console.log(
{ filetype },
{ attrs },
{ oldCode: _oldCode.slice(0, 30) }
);
return (
"```" +
filetype +
' filename="' +
srcFilename +
'"' +
attrs +
"\n" +
srcStr +
"```"
);
})
);
}
await genDocsMd();
}

View File

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

View File

@@ -1,35 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,3 +0,0 @@
{
"tabWidth": 2
}

View File

@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -1,49 +0,0 @@
"use client";
import { useLayoutEffect, useState, useRef, IframeHTMLAttributes } from "react";
export function ResponsiveIframe(
props: IframeHTMLAttributes<HTMLIFrameElement>
) {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [url, setUrl] = useState<string | undefined>(props.src);
useLayoutEffect(() => {
const listener = (e: MessageEvent) => {
console.log(e);
if (e.data.type === "navigate" && props.src?.startsWith(e.origin)) {
setUrl(e.data.url);
}
};
window.addEventListener("message", listener);
return () => {
window.removeEventListener("message", listener);
};
}, [props.src]);
useLayoutEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver(() => {
if (!containerRef.current) return;
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight,
});
});
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, [containerRef]);
return (
<div className={"w-full h-full flex flex-col " + props.className} >
<input className="text-xs p-2" value={url} readOnly/>
<div className="flex-grow" ref={containerRef}>
<iframe {...props} className="" {...dimensions} allowFullScreen/>
</div>
</div>
);
}

View File

@@ -1,51 +0,0 @@
export function Slogan(props: { children: React.ReactNode, small?: boolean }) {
return (
<div className={"leading-snug mb-5 max-w-3xl text-neutral-700 dark:text-neutral-200 " + (props.small ? "text-lg mt-2" : "text-2xl mt-5")}>
{props.children}
</div>
);
}
export function Grid(props: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-10 items-stretch">
{props.children}
</div>
);
}
export function GridItem(props: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={
(props.className || "") +
" [&>.nextra-code-block]:h-full [&>.nextra-code-block>pre]:h-full [&>.nextra-code-block>pre]:mb-0"
}
>
{props.children}
</div>
);
}
export function GridCard(props: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={
"border border-stone-200 dark:border-stone-500 rounded-xl p-4 [&>h4]:mt-0 [&>h3]:mt-0 " +
props.className
}
>
{props.children}
</div>
);
}
export function GoogleLogo() {
return <svg className="w-3 h-3 inline align-baseline" viewBox="0 0 950 950" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M915.2 448l-4.2-17.8H524V594h231.2c-24 114-135.4 174-226.4 174-66.2 0-136-27.8-182.2-72.6-47.4-46-77.6-113.8-77.6-183.6 0-69 31-138 76.2-183.4 45-45.2 113.2-70.8 181-70.8 77.6 0 133.2 41.2 154 60l116.4-115.8c-34.2-30-128-105.6-274.2-105.6-112.8 0-221 43.2-300 122C144.4 295.8 104 408 104 512s38.2 210.8 113.8 289c80.8 83.4 195.2 127 313 127 107.2 0 208.8-42 281.2-118.2 71.2-75 108-178.8 108-287.6 0-45.8-4.6-73-4.8-74.2z" fill="currentColor" /></svg>
}

View File

@@ -1,9 +0,0 @@
const withNextra = require('nextra')({
theme: 'nextra-theme-docs',
themeConfig: './theme.config.jsx',
mdxOptions: {
}
})
module.exports = withNextra()

View File

@@ -1,29 +0,0 @@
{
"name": "homepage-jazz",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^13.5.3",
"nextra": "^2.13.1",
"nextra-theme-docs": "^2.13.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"autoprefixer": "latest",
"eslint": "latest",
"eslint-config-next": "latest",
"postcss": "latest",
"tailwindcss": "latest",
"typescript": "latest"
}
}

View File

@@ -1,15 +0,0 @@
import './globals.css'
import { Manrope } from 'next/font/google'
import { Inter } from 'next/font/google'
import localFont from 'next/font/local'
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({ subsets: ['latin'], variable: '--font-manrope', })
const inter = Inter({ subsets: ['latin'], variable: '--font-inter', })
const pragmata = localFont({src: "../fonts/PragmataProR_0829.woff2", subsets: ['latin'], variable: '--font-pragmata'})
// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }) {
return <div className={manrope.variable + " " + pragmata.variable + " " + inter.className + " font-[450]"}><Component {...pageProps} /></div>
}

View File

@@ -1,41 +0,0 @@
{
"index": {
"title": "Introduction",
"theme": {
"typesetting": "article",
"layout": "full"
},
"type": "page"
},
"examples": {
"title": "Example Gallery",
"theme": {
"typesetting": "article",
"layout": "full"
},
"type": "page"
},
"mesh": {
"title": "Global Mesh & Pricing",
"theme": {
"typesetting": "article",
"layout": "full"
},
"type": "page"
},
"guides": {
"title": "Guides",
"type": "page",
"theme": {
"breadcrumb": true,
"footer": true,
"sidebar": true,
"toc": true,
"pagination": true
}
},
"docs": {
"title": "API Docs",
"type": "page"
}
}

View File

@@ -1 +0,0 @@
# API docs

View File

@@ -1 +0,0 @@
# Something

View File

@@ -1 +0,0 @@
# Something else

View File

@@ -1,162 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body article.nextra-content h1 {
font-family: var(--font-manrope);
}
body article.nx-w-full h1 {
font-family: var(--font-manrope);
text-align: left;
@apply tracking-tight;
@apply text-6xl;
@apply font-medium;
}
body article.nx-w-full h1:first-of-type {
@apply mt-20;
}
body article.nx-w-full h2 {
font-family: var(--font-manrope);
text-align: left;
@apply tracking-tight;
@apply text-3xl;
@apply font-[600];
}
body article.nx-w-full p {
@apply max-w-3xl;
}
body pre code.nx-text-\[\.9em\] {
font-size: 0.8rem;
line-height: 1.35;
}
[style="color:var(--shiki-token-keyword)"]+[style="color:var(--shiki-token-string-expression)"]:after {
content: "⋯";
text-indent: 0;
display: block;
line-height: initial;
letter-spacing: normal;
outline: 1px solid var(--shiki-token-string-expression);
border-radius: 0.25rem;
margin-left: 0.1rem;
margin-right: 0.1rem;
}
[style="color:var(--shiki-token-keyword)"]+[style="color:var(--shiki-token-string-expression)"] {
position: relative;
opacity: 1;
display: inline-block;
text-indent: -9999px;
line-height: 0;
opacity: 0.5;
transition: opacity 0.2s;
}
[style="color:var(--shiki-token-keyword)"]+[style="color:var(--shiki-token-string-expression)"]:hover {
text-indent: 0;
line-height: initial;
opacity: 1;
}
[style="color:var(--shiki-token-keyword)"]+[style="color:var(--shiki-token-string-expression)"]:hover:after {
display: none;
}
body .nextra-card svg {
color: black;
opacity: 0.2;
transition: opacity 0.2s ease;
}
body .nextra-card:hover svg {
color: black;
opacity: 0.8;
}
.dark body .nextra-card svg {
color: white;
opacity: 0.4;
}
.dark body .nextra-card:hover svg {
color: white;
opacity: 0.8;
}
/* @media screen and (min-width: 80rem) {
body article.nx-w-full {
@apply -mx-32;
}
} */
:root {
--nextra-primary-hue: 30deg;
--nextra-primary-saturation: 15%;
}
/* .nextra-nav-container nav {
justify-content: flex-start;
max-width: 70rem;
} */
.nextra-nav-container nav :first-child {
margin-right: 0;
}
.nextra-nav-container nav a:not(:first-child) {
padding-left: 1rem;
padding-right: 1rem;
}
.nextra-search+* {
margin-left: auto;
}
body code, body kbd, body samp, body pre {
font-family: var(--font-pragmata);
}
body code[data-line-numbers]>.line {
padding-left: 0.25rem;
}
body code[data-line-numbers]>.line:before {
--tw-text-opacity: 0.3;
min-width: 1.5rem;
font-size: 0.7rem;
padding-right: 0.5rem;
position: relative;
top: 0.07rem;
}
body {
--shiki-color-text: #606060;
--shiki-color-background: transparent;
--shiki-token-constant: #00a5a5;
--shiki-token-string: #1aa245;
--shiki-token-comment: #aaa;
--shiki-token-keyword: #7b8bff;
--shiki-token-parameter: #ff9800;
--shiki-token-function: #445dd7;
--shiki-token-string-expression: #1aa245;
--shiki-token-punctuation: #969696;
--shiki-token-link: #1aa245;
}
.dark body {
--shiki-color-text: #d1d1d1;
--shiki-token-constant: #2DC9C9;
--shiki-token-string: #ffab70;
--shiki-token-comment: #6b737c;
--shiki-token-keyword: #7b8bff;
--shiki-token-parameter: #ff9800;
--shiki-token-function: #9BABFF;
--shiki-token-string-expression: #42BB69;
--shiki-token-punctuation: #bbb;
--shiki-token-link: #ffab70;
}

View File

@@ -1 +0,0 @@
# Guides

View File

@@ -1,3 +0,0 @@
{
"gettingStarted": "Getting Started"
}

View File

@@ -1 +0,0 @@
# Getting started

View File

@@ -1,237 +0,0 @@
import { Tabs, Cards, Card } from "nextra/components";
import { Slogan, Grid, GridItem, GridCard, GoogleLogo } from "../components";
import { ResponsiveIframe } from "../components/ResponsiveIframe";
import {
ArrowUpDownIcon,
UploadCloudIcon,
PlaneIcon,
MonitorSmartphoneIcon,
TextCursorIcon,
MousePointer2Icon,
GaugeIcon,
HandIcon
} from "lucide-react";
# Instant sync.
<Slogan>Go beyond request/response &mdash; ship modern apps with sync.</Slogan>
Jazz is an open-source toolkit for building apps with **sync** and **secure collaborative data.**
<h2 className="mt-24">Hard things are easy now.</h2>
Jazz takes what *backends* + *databases* + *CDNs* + *real-time infrastructure* do, generalizes the problem and solves it in a completely new way. (How? Keep reading.)
Because of that, with Jazz, you only build what makes your app *your app:*<br/>1. **Define your data model.** -> 2. **Add role-based permissions.** -> 3. **Build your UI.**
And you get **built-in capabilities** that took the &ldquo;big ones&rdquo; <small>(GDocs,&nbsp;Figma,&nbsp;Notion,&nbsp;Linear,&nbsp;&hellip;)</small> *years* to build:
<Cards>
<Card href="#" title="Cross-device sync" icon={<MonitorSmartphoneIcon />} />
<Card
href="#"
title="Real-time multiplayer"
icon={
<div className="w-6 h-6 flex flex-col">
<TextCursorIcon
size="10"
absoluteStrokeWidth
className="-scale-x-100 self-start -ml-1"
/>
<MousePointer2Icon size="15" absoluteStrokeWidth className="-mt-1 -mx-1.5 self-end"/>
<HandIcon size="15" absoluteStrokeWidth className="-mt-2 -mx-1.5 self-start"/>
</div>
}
/>
<Card href="#" title="Automatic granular data-fetching" icon={<ArrowUpDownIcon />} />
<Card href="#" title="Cloud persistence & Local storage" icon={<UploadCloudIcon />} />
<Card
href="#"
title="Offline support & sync-when-possible"
icon={<PlaneIcon />}
/>
<Card href="#" title="Fluid UI perf & 90% less loading" icon={<GaugeIcon />}/>
</Cards>
## First impressions&hellip;
<Slogan small>A chat app in 86 lines of code.</Slogan>
<Grid>
<GridItem>
```tsx filename="dataModel.ts" showLineNumbers
import { CoMap, CoList } from 'cojson';
export type Chat = CoList<Message['id']>;
export type Message = CoMap<{ text: string }>;
```
</GridItem>
<GridItem className="col-start-1">
```tsx filename="app.tsx" showLineNumbers
import { WithJazz, useJazz, DemoAuth } from 'jazz-react';
import ReactDOM from 'react-dom/client';
import { HashRoute } from 'hashroute';
import { ChatWindow } from './chatWindow.tsx';
import { Chat } from './dataModel.ts';
ReactDOM.createRoot(document.getElementById('root')!).render(
<WithJazz auth={DemoAuth({ appName: 'Chat' })}>
<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 />,
'/:id': (id) => <ChatWindow chatId={id as Chat['id']} />,
}, { reportToParentFrame: true })}
</div>
}
function Home() {
const { me } = useJazz();
// Groups determine access rights to values they own.
const createChat = () => {
const group = me.createGroup().addMember('everyone', 'writer');
const chat = group.createList<Chat>();
location.hash = '/' + chat.id;
};
return <button onClick={createChat} className='rounded py-2 px-4 bg-stone-200 dark:bg-stone-800 dark:text-white my-auto'>
Create New Chat
</button>
}
````
</GridItem>
<GridItem className="col-start-2 row-start-1 row-span-2">
```tsx filename="chatWindow.tsx" showLineNumbers
import { useAutoSub } from 'jazz-react';
import { Chat, Message } from './dataModel.ts';
export function ChatWindow({ chatId }: { chatId: Chat['id'] }) {
const chat = useAutoSub(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}
time={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({ text, by, time: t, byMe }:
{ text?: string, by?: string, time?: Date, byMe?: boolean }
) {
return <div className={`items-${byMe ? 'end' : '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]'>
{ text }
</div>
<div className='text-xs text-neutral-500 ml-2'>
{ by } { t?.getHours() }:{ t?.getMinutes() }
</div>
</div>;
}
function ChatInput({ onSubmit }: { 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;
onSubmit(input.value);
input.value = '';
}}/>
}
````
</GridItem>
<ResponsiveIframe src="http://localhost:9999/" className="col-start-3 row-start-1 row-span-2 rounded-xl overflow-hidden border dark:border-stone-700 min-h-[50vh]"/>
</Grid>
## How does it work?
<Slogan small>Introducing: Secure collaborative data.</Slogan>
Jazz is built around **CoJSON,** a new abstraction that implements **multi-device co-editing,** **user identities & permissions** and **sync & persistence** in a standardized way with a high-level API.
This makes collaboration and secure access control feel like **inherent properties of your data** &mdash;&nbsp;so&nbsp;we're calling it &ldquo;secure collaborative data.&rdquo;
### Collaborative Values
<Slogan small>Your new building blocks.</Slogan>
- Data that multiple users can co-edit in real time or async with smart conflict resolution
<Grid>
<GridCard>
#### `CoMap`s - Key-value maps
</GridCard>
<GridCard>
#### `CoList`s - Ordered lists
</GridCard>
<GridCard>
#### `CoString`s - Plain-text
</GridCard>
<GridCard>
#### `CoText`s - Rich-text
- Generic collaborative markup format that prevents most editing conflicts
</GridCard>
<GridCard>
#### `CoStream`s - Per-user value streams
- Enforce per-user separation for user presence, social reactions, polls, replies etc.
</GridCard>
<GridCard>
#### `BinaryCoStream`s - file/media streams
- Create, reference and load even huge binary blobs or create live-streams without needing external services
</GridCard>
</Grid>
### Accounts & Groups
<Slogan small>First-class user identities & secure permissions.</Slogan>
- Simple API to define groups of users, their roles
- Verifiably enforced by encryption and signatures
## Jazz: batteries included.
<Grid>
<GridCard>
### Auto-sub
<Slogan small>Let your UI drive data-syncing</Slogan>
</GridCard>
<GridCard>
### Auth providers
</GridCard>
<GridCard>
### Two-way sync to your existing database
</GridCard>
</Grid>
## Global Mesh

View File

@@ -1,7 +0,0 @@
import { Slogan } from './index.mdx'
# Jazz Global Mesh
<Slogan>Serverless sync and storage for Jazz apps.</Slogan>
Real-time syncing infrastructure that scales up to millions of users. Pricing that scales down to zero.

View File

@@ -1,15 +0,0 @@
module.exports = {
darkMode: 'class',
content: [
'./pages/**/*.{js,jsx,ts,tsx,md,mdx}',
'./components/**/*.{js,jsx,ts,tsx,md,mdx}',
'./theme.config.jsx'
],
theme: {
extend: {
display: ['var(--font-manrope)'],
mono: ['var(--font-pragmata)'],
}
},
plugins: []
}

View File

@@ -1,62 +0,0 @@
import Link from "next/link";
export default {
logo: (
<svg
width={386 / 4}
height={146 / 4}
viewBox="0 0 386 146"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M176.725 33.865H188.275V22.7H176.725V33.865ZM164.9 129.4H172.875C182.72 129.4 188.275 123.9 188.275 114.22V43.6H176.725V109.545C176.725 115.65 173.975 118.51 167.925 118.51H164.9V129.4ZM245.298 53.28C241.613 45.47 233.363 41.95 222.748 41.95C208.998 41.95 200.748 48.44 197.888 58.615L208.613 61.915C210.648 55.315 216.368 52.565 222.638 52.565C231.933 52.565 235.673 56.415 236.058 64.61C226.433 65.93 216.643 67.195 209.768 69.23C200.583 72.145 195.743 77.865 195.743 86.83C195.743 96.51 202.673 104.65 215.818 104.65C225.443 104.65 232.318 101.35 237.213 94.365V103H247.388V66.425C247.388 61.475 247.168 57.185 245.298 53.28ZM217.853 95.245C210.483 95.245 207.128 91.34 207.128 86.72C207.128 82.045 210.593 79.515 215.323 77.92C220.328 76.435 226.983 75.5 235.948 74.18C235.893 76.93 235.673 80.725 234.738 83.475C233.418 89.25 227.643 95.245 217.853 95.245ZM251.22 103H301.545V92.715H269.535L303.195 45.47V43.6H254.3V53.885H284.935L251.22 101.185V103ZM304.815 103H355.14V92.715H323.13L356.79 45.47V43.6H307.895V53.885H338.53L304.815 101.185V103Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M136.179 44.8277C136.179 44.8277 136.179 44.8277 136.179 44.8276V21.168C117.931 28.5527 97.9854 32.6192 77.0897 32.6192C65.1466 32.6192 53.5138 31.2908 42.331 28.7737V51.4076C42.331 51.4076 42.331 51.4076 42.331 51.4076V81.1508C41.2955 80.4385 40.1568 79.8458 38.9405 79.3915C36.1732 78.358 33.128 78.0876 30.1902 78.6145C27.2524 79.1414 24.5539 80.4419 22.4358 82.3516C20.3178 84.2613 18.8754 86.6944 18.291 89.3433C17.7066 91.9921 18.0066 94.7377 19.1528 97.2329C20.2991 99.728 22.2403 101.861 24.7308 103.361C27.2214 104.862 30.1495 105.662 33.1448 105.662H33.1455C33.6061 105.662 33.8365 105.662 34.0314 105.659C44.5583 105.449 53.042 96.9656 53.2513 86.4386C53.2534 86.3306 53.2544 86.2116 53.2548 86.0486H53.2552V85.7149L53.2552 85.5521V82.0762L53.2552 53.1993C61.0533 54.2324 69.0092 54.7656 77.0897 54.7656C77.6696 54.7656 78.2489 54.7629 78.8276 54.7574V110.696C77.792 109.983 76.6533 109.391 75.437 108.936C72.6697 107.903 69.6246 107.632 66.6867 108.159C63.7489 108.686 61.0504 109.987 58.9323 111.896C56.8143 113.806 55.3719 116.239 54.7875 118.888C54.2032 121.537 54.5031 124.283 55.6494 126.778C56.7956 129.273 58.7368 131.405 61.2273 132.906C63.7179 134.406 66.646 135.207 69.6414 135.207C70.1024 135.207 70.3329 135.207 70.5279 135.203C81.0548 134.994 89.5385 126.51 89.7478 115.983C89.7517 115.788 89.7517 115.558 89.7517 115.097V111.621L89.7517 54.3266C101.962 53.4768 113.837 51.4075 125.255 48.2397V80.9017C124.219 80.1894 123.081 79.5966 121.864 79.1424C119.097 78.1089 116.052 77.8384 113.114 78.3653C110.176 78.8922 107.478 80.1927 105.36 82.1025C103.242 84.0122 101.799 86.4453 101.215 89.0941C100.631 91.743 100.931 94.4886 102.077 96.9837C103.223 99.4789 105.164 101.612 107.655 103.112C110.145 104.612 113.073 105.413 116.069 105.413C116.53 105.413 116.76 105.413 116.955 105.409C127.482 105.2 135.966 96.7164 136.175 86.1895C136.179 85.9945 136.179 85.764 136.179 85.3029V81.8271L136.179 44.8277Z"
fill="#3313F7"
/>
</svg>
),
project: {
link: "https://github.com/gardencmp/jazz",
},
docsRepositoryBase:
"https://github.com/gardencmp/jazz/tree/main/homepage/homepage-jazz",
chat: { link: "https://discord.gg/utDMjHYg42" },
navbar: {
extraContent: (
<Link
className="nx-p-2 nx-text-current"
href={"https://twitter.com/jazz_tools"}
target="_blank"
>
<svg width="24" height="24" viewBox="0 0 248 204">
<path
fill="currentColor"
d="M221.95 51.29c.15 2.17.15 4.34.15 6.53 0 66.73-50.8 143.69-143.69 143.69v-.04c-27.44.04-54.31-7.82-77.41-22.64 3.99.48 8 .72 12.02.73 22.74.02 44.83-7.61 62.72-21.66-21.61-.41-40.56-14.5-47.18-35.07a50.338 50.338 0 0 0 22.8-.87C27.8 117.2 10.85 96.5 10.85 72.46v-.64a50.18 50.18 0 0 0 22.92 6.32C11.58 63.31 4.74 33.79 18.14 10.71a143.333 143.333 0 0 0 104.08 52.76 50.532 50.532 0 0 1 14.61-48.25c20.34-19.12 52.33-18.14 71.45 2.19 11.31-2.23 22.15-6.38 32.07-12.26a50.69 50.69 0 0 1-22.2 27.93c10.01-1.18 19.79-3.86 29-7.95a102.594 102.594 0 0 1-25.2 26.16z"
/>
</svg>
</Link>
),
},
useNextSeoProps() {
return {
titleTemplate: "jazz %s",
};
},
footer: {
text: (
<span>
MIT {new Date().getFullYear()} ©{" "}
<a href="https://gcmp.io" target="_blank">
Garden Computing, Inc
</a>
.
</span>
),
}
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -75,6 +75,9 @@
body {
@apply bg-background text-foreground;
}
.overlay-close {
background-color: "black";
}
}
pre.shiki {

View File

@@ -8,19 +8,20 @@ import localFont from "next/font/local";
import { GcmpLogo, JazzLogo } from "@/components/logos";
import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
import { Nav } from "@/components/nav";
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
import { MailIcon } from "lucide-react";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope" });
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const pragmata = localFont({
src: "../fonts/PragmataProR_0829.woff2",
variable: "--font-pragmata",
src: "../fonts/ppr_0829.woff2",
variable: "--font-ppr",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "jazz - Instant sync",
description: "Go beyond request/response - ship modern apps with sync.",
};
export default function RootLayout({
@@ -49,33 +50,43 @@ export default function RootLayout({
items={[
{ title: "Toolkit", href: "/" },
{ title: "Global Mesh", href: "/mesh" },
{ title: "Docs & Guides", href: "/docs" },
{
title: "Docs & Guides",
href: "https://github.com/gardencmp/jazz/blob/main/DOCS.md",
newTab: true,
},
{
title: "Blog",
href: "https://gcmp.io/news",
firstOnRight: true,
newTab: true,
},
{
title: "Releases",
href: "https://github.com/gardencmp/jazz/releases",
newTab: true,
},
{
title: "Roadmap",
href: "https://github.com/orgs/gardencmp/projects/4/views/3",
newTab: true,
},
{
title: "GitHub",
href: "https://github.com/gardencmp/jazz",
newTab: true,
icon: <SiGithub className="w-5" />,
},
{
title: "Discord",
href: "https://discord.gg/utDMjHYg42",
newTab: true,
icon: <SiDiscord className="w-5" />,
},
{
title: "X",
href: "https://x.com/jazz_tools",
newTab: true,
icon: <SiTwitter className="w-5" />,
},
]}
@@ -90,34 +101,105 @@ export default function RootLayout({
"prose-h2:text-2xl lg:prose-h2:text-3xl prose-h2:font-medium prose-h2:tracking-tight",
"prose-p:max-w-3xl prose-p:leading-snug",
"prose-strong:font-medium",
"prose-code:leading-tight prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:-my-1 prose-code:rounded",
"prose-code:font-normal prose-code:leading-tight prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:-my-1 prose-code:rounded",
].join(" ")}
>
{children}
</article>
</main>
<footer className="flex mt-10 min-h-[15rem] -mb-20 bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
<div className="p-8 max-w-[80rem] w-full flex gap-4">
<div className="flex-1 flex flex-col gap-2 text-sm">
<div className="p-8 max-w-[80rem] w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
<GcmpLogo monochrome className="w-32" />
<p className="mt-auto">
<p className="max-sm:text-right">
© 2023
<br />
Garden Computing, Inc.
</p>
</div>
<div className="flex-1 flex flex-col gap-2 text-sm">
{/* <h1 className="font-medium">Resources</h1> */}
<div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">Resources</h1>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="/"
>
Toolkit
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="/mesh"
>
Global Mesh
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/gardencmp/jazz/blob/main/DOCS.md"
newTab
>
Docs & Guides
</NavLink>
</div>
<div className="flex-1 flex flex-col gap-2 text-sm">
{/* <h1 className="font-medium">Legal</h1> */}
{/* <div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">Legal</h1>
</div> */}
<div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">Community</h1>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/gardencmp/jazz"
newTab
>
GitHub
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://discord.gg/utDMjHYg42"
newTab
>
Discord
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://x.com/jazz_tools"
newTab
>
Twitter
</NavLink>
</div>
<div className="flex-1 flex flex-col gap-2 text-sm">
{/* <h1 className="font-medium">Newsletter</h1> */}
<div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">News</h1>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://gcmp.io/news"
newTab
>
Blog
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/gardencmp/jazz/releases"
newTab
>
Releases
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/orgs/gardencmp/projects/4/views/3"
newTab
>
Roadmap
</NavLink>
</div>
<div className="col-span-3 md:col-start-2 lg:col-start-auto flex flex-col gap-2 text-sm">
Sign up for updates:
<Newsletter/>
</div>
</div>
</footer>
</ThemeProvider>
<script defer data-api="/api/event" data-domain="jazz.tools" src="/js/script.js"></script>
</body>
</html>
);

View File

@@ -1,5 +1,10 @@
import { Slogan, Grid, GridCard } from '@/components/forMdx';
import { Pricing } from '@/components/pricing';
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Global Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz Global Mesh
@@ -10,7 +15,7 @@ Pricing that scales down to zero.
## The first Collaboration Delivery Network
<Slogan small>Build demanding apps with write-heavy distributed state, backed by a new kind of cloud.</Slogan>
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
<Grid>
<GridCard>
@@ -26,6 +31,7 @@ Give users instant load times, with their latest data state always cached close
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
@@ -33,18 +39,120 @@ Give users instant load times, with their latest data state always cached close
<Slogan small></Slogan>
<Pricing />
### Free Tier
<span className="text-lg font-medium bg-emerald-200 dark:bg-emerald-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</span>
<p className="text-sm">Later, any usage under $1/mo will be free.</p>
### Transactions explained
<Grid>
<GridItem className="md:col-span-2">
### Unlimited <ComingSoonBadge/>
<div className="lg:text-2xl border rounded-lg px-1 py-3 text-center">${pricePer1MtxSyncedOut} <small>per 1M TXs synced out</small> + ${pricePer1MtxStored}<small>/mo per 1M TXs stored</small></div>
<p><small>$6/mo minimum usage</small></p>
A TX (transaction) represents an **individual user action**, or **up to 100KB of binary data**.
</GridItem>
<GridItem className="col-start-1">
#### Transactions synced out:
<div className="text-sm">
- Transactions sent out from Global Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each transaction should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Transactions stored:
<div className="text-sm">
- Transactions that are continuously persisted.
- Counted per second.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of transactions generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- 4 users co-editing 10 pages of text, typing them out as individual character inserts:
- 3,000 inserts/page &times; 10 pages = 30,000 transactions
- 30,000 transactions stored = ${30000 * pricePerTxStored} / mo
- 3 &times; 30,000 transactions synced out = ${3 * 30000 * pricePerTxSyncedOut} one-time
- 4 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 2h/day for a month
- 4 users &times; 10 FPS &times; 10s/min &times; 60min/h * 2h/day &times; 30days = 1.44M transactions
- 1.44M transactions stored = ${1440000 * pricePerTxStored} / mo = ${1440000 * pricePerTxStored / 4} / mo / user
- 3 &times; 1.44M transactions synced out = ${(3 * 1440000 * pricePerTxSyncedOut).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time = ${(3 * 1440000 * pricePerTxSyncedOut / 4).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time / user
- A livestreamer streaming video (1GB total) to 100 viewers (combined live & on-demand)
- 1GB = 10,000 transactions (100KB each)
- 10,000 transactions stored = ${10000 * pricePerTxStored} / mo (= ${10000 * 1000 * pricePerTxStored} per 1TB stored)
- 100 &times; 10,000 transaction synced out = ${100 * 10000 * pricePerTxSyncedOut} one-time (= ${10000 * 1000 * pricePerTxSyncedOut} per 1TB egress)
</div>
## Global Footprint
Currently we are running endpoints in the following locations:
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
- Los Angeles
- New Jersey
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios

View File

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

File diff suppressed because one or more lines are too long

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