Compare commits
54 Commits
hashroute@
...
jazz-nodej
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46fcbd6c01 | ||
|
|
aa3e3de09e | ||
|
|
af3d48764d | ||
|
|
091f36b736 | ||
|
|
7107f79f42 | ||
|
|
9922db2336 | ||
|
|
75db570198 | ||
|
|
28a09f377b | ||
|
|
fd2e0855bb | ||
|
|
82e1d57bd6 | ||
|
|
a2fbb0b0c8 | ||
|
|
8feddf9932 | ||
|
|
feed34b1cf | ||
|
|
662c980cf2 | ||
|
|
f5ae530890 | ||
|
|
46bf7dd3ce | ||
|
|
5d4eb38204 | ||
|
|
66da658075 | ||
|
|
3477b74573 | ||
|
|
f3de4906b7 | ||
|
|
caded3f189 | ||
|
|
5196395495 | ||
|
|
8089a7ed9f | ||
|
|
99230d31d2 | ||
|
|
94bca03f59 | ||
|
|
49719b6e6d | ||
|
|
1bdb781452 | ||
|
|
c336f69a6b | ||
|
|
c8cb1ce208 | ||
|
|
814a6a80cd | ||
|
|
5fdfe18b32 | ||
|
|
7b7a74778b | ||
|
|
39dbd46556 | ||
|
|
1db4a14be4 | ||
|
|
4a4ea4e196 | ||
|
|
e0724441eb | ||
|
|
5d47895515 | ||
|
|
c1dfac7260 | ||
|
|
bf29cb3bae | ||
|
|
a0a9b3f851 | ||
|
|
46330ae201 | ||
|
|
bfe3595b4c | ||
|
|
34c39e6a55 | ||
|
|
5a85501919 | ||
|
|
97a4282e5e | ||
|
|
39c13b50a3 | ||
|
|
ad304e321b | ||
|
|
72fce45b2b | ||
|
|
1f49d7fda6 | ||
|
|
eec8ee7027 | ||
|
|
188eb2e1e3 | ||
|
|
62867b32d9 | ||
|
|
ccebd2447d | ||
|
|
08dca75789 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
74
.github/workflows/build-and-deploy.yaml
vendored
74
.github/workflows/build-and-deploy.yaml
vendored
@@ -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
|
||||
9
examples/chat/CHANGELOG.md
Normal file
9
examples/chat/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.5.0
|
||||
- jazz-react-auth-local@0.4.16
|
||||
@@ -3,7 +3,7 @@ job "chat$BRANCH_SUFFIX" {
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 8
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
18
examples/file-drop/.eslintrc.cjs
Normal file
18
examples/file-drop/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
24
examples/file-drop/.gitignore
vendored
Normal file
24
examples/file-drop/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
9
examples/file-drop/CHANGELOG.md
Normal file
9
examples/file-drop/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# jazz-example-file-drop
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.5.0
|
||||
- jazz-react-auth-local@0.4.16
|
||||
4
examples/file-drop/Dockerfile
Normal file
4
examples/file-drop/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM caddy:2.7.3-alpine
|
||||
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||
|
||||
COPY ./dist /usr/share/caddy/
|
||||
64
examples/file-drop/README.md
Normal file
64
examples/file-drop/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Jazz Todo List Example
|
||||
|
||||
Live version: https://example-todo.jazz.tools
|
||||
|
||||
## Installing & running the example locally
|
||||
|
||||
Start by checking out just the example app to a folder:
|
||||
|
||||
```bash
|
||||
npx degit gardencmp/jazz/examples/todo jazz-example-todo
|
||||
cd jazz-example-todo
|
||||
```
|
||||
|
||||
(This ensures that you have the example app without git history or our multi-package monorepo)
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Start the dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
|
||||
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
|
||||
- [`src/1_types.ts`](./src/1_types.ts),
|
||||
[`src/2_main.tsx`](./src/2_main.tsx),
|
||||
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
|
||||
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
|
||||
|
||||
## Walkthrough
|
||||
|
||||
### Main parts
|
||||
|
||||
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
|
||||
|
||||
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
|
||||
|
||||
3. Creating a new todo project: [`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx)
|
||||
|
||||
4. Reactively rendering a todo project as a table, adding and editing tasks: [`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx)
|
||||
|
||||
### Helpers
|
||||
|
||||
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
|
||||
|
||||
This is the whole Todo List app!
|
||||
|
||||
## Questions / problems / feedback
|
||||
|
||||
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||
|
||||
|
||||
## Configuration: sync server
|
||||
|
||||
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||
|
||||
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).
|
||||
16
examples/file-drop/components.json
Normal file
16
examples/file-drop/components.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/basicComponents",
|
||||
"utils": "@/basicComponents/lib/utils"
|
||||
}
|
||||
}
|
||||
13
examples/file-drop/index.html
Normal file
13
examples/file-drop/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz File Drop Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/2_main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
56
examples/file-drop/job-template.nomad
Normal file
56
examples/file-drop/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
||||
job "example-file-drop$BRANCH_SUFFIX" {
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
to = 80
|
||||
}
|
||||
}
|
||||
|
||||
constraint {
|
||||
attribute = "${node.class}"
|
||||
operator = "="
|
||||
value = "mesh"
|
||||
}
|
||||
|
||||
spread {
|
||||
attribute = "${node.datacenter}"
|
||||
weight = 100
|
||||
}
|
||||
|
||||
constraint {
|
||||
distinct_hosts = true
|
||||
}
|
||||
|
||||
task "server" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "$DOCKER_TAG"
|
||||
ports = ["http"]
|
||||
|
||||
auth = {
|
||||
username = "$DOCKER_USER"
|
||||
password = "$DOCKER_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
name = "example-file-drop$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 50 # MHz
|
||||
memory = 50 # MB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# deploy bump 4
|
||||
46
examples/file-drop/package.json
Normal file
46
examples/file-drop/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "jazz-example-file-drop",
|
||||
"private": true,
|
||||
"version": "0.0.63",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 6610",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "^0.5.0",
|
||||
"jazz-react-auth-local": "^0.4.16",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uniqolor": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
BIN
examples/file-drop/public/jazz-logo.png
Normal file
BIN
examples/file-drop/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
5
examples/file-drop/src/1_types.ts
Normal file
5
examples/file-drop/src/1_types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CoMap, BinaryCoStream } from "cojson";
|
||||
|
||||
export type FileBundle = CoMap<{
|
||||
[filename: string]: BinaryCoStream['id']
|
||||
}>;
|
||||
187
examples/file-drop/src/2_main.tsx
Normal file
187
examples/file-drop/src/2_main.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { ChangeEvent, useCallback, useState } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
useNavigate,
|
||||
useParams,
|
||||
} from "react-router-dom";
|
||||
import "./index.css";
|
||||
|
||||
import { WithJazz, useJazz, useAcceptInvite, useAutoSub } from "jazz-react";
|
||||
import { LocalAuth } from "jazz-react-auth-local";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
ThemeProvider,
|
||||
TitleAndLogo,
|
||||
} from "./basicComponents/index.ts";
|
||||
import { PrettyAuthUI } from "./components/Auth.tsx";
|
||||
import { FileBundle } from "./1_types.ts";
|
||||
import {
|
||||
createBinaryStreamFromBlob,
|
||||
readBlobFromBinaryStream,
|
||||
} from "jazz-browser";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
|
||||
const appName = "Jazz File Drop Example";
|
||||
|
||||
const auth = LocalAuth({
|
||||
appName,
|
||||
Component: PrettyAuthUI,
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<TitleAndLogo name={appName} />
|
||||
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
|
||||
<WithJazz auth={auth}>
|
||||
<App />
|
||||
</WithJazz>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
function App() {
|
||||
// logOut logs out the AuthProvider passed to `<WithJazz/>` above.
|
||||
const { logOut } = useJazz();
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <FileDropUI />,
|
||||
},
|
||||
{
|
||||
path: "/bundle/:bundleId",
|
||||
element: <FileDropUIPage />,
|
||||
},
|
||||
{
|
||||
path: "/invite/*",
|
||||
element: <p>Accepting invite...</p>,
|
||||
},
|
||||
]);
|
||||
|
||||
// `useAcceptInvite()` is a hook that accepts an invite link from the URL hash,
|
||||
// and on success calls our callback where we navigate to the project that we were just invited to.
|
||||
useAcceptInvite((bundleId) => router.navigate("/v/" + bundleId));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterProvider router={router} />
|
||||
|
||||
<Button
|
||||
onClick={() => router.navigate("/").then(logOut)}
|
||||
variant="outline"
|
||||
>
|
||||
Log Out
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileDropUIPage() {
|
||||
const { bundleId } = useParams<{ bundleId: FileBundle["id"] }>();
|
||||
|
||||
return <FileDropUI bundleId={bundleId} />;
|
||||
}
|
||||
|
||||
export function FileDropUI({ bundleId }: { bundleId?: FileBundle["id"] }) {
|
||||
const navigate = useNavigate();
|
||||
const { me, localNode } = useJazz();
|
||||
const fileBundle = useAutoSub(bundleId);
|
||||
|
||||
const [progressMessage, setProgressMessage] = useState<{
|
||||
[name: string]: string;
|
||||
}>({});
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
let fileBundleToUse = fileBundle?.meta.coValue;
|
||||
let isFirstUpload = false;
|
||||
|
||||
if (!fileBundleToUse) {
|
||||
const group = me.createGroup().addMember("everyone", "reader");
|
||||
fileBundleToUse = group.createMap<FileBundle>();
|
||||
isFirstUpload = true;
|
||||
}
|
||||
|
||||
const files = [...(event.target.files || [])];
|
||||
|
||||
Promise.all(
|
||||
files.map((file) =>
|
||||
createBinaryStreamFromBlob(
|
||||
file,
|
||||
fileBundleToUse!.group,
|
||||
{ type: "binary" },
|
||||
(progress) =>
|
||||
setProgressMessage((old) => ({
|
||||
...old,
|
||||
[file.name]: `Creating ${Math.round(
|
||||
progress * 100
|
||||
)}%`,
|
||||
}))
|
||||
).then((stream) => {
|
||||
fileBundleToUse!.set(file.name, stream.id);
|
||||
})
|
||||
)
|
||||
).then(() => {
|
||||
if (isFirstUpload) {
|
||||
navigate("/bundle/" + fileBundleToUse!.id);
|
||||
}
|
||||
});
|
||||
|
||||
event.target.value = "";
|
||||
},
|
||||
[me, navigate, fileBundle]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-full p-5 w-[40rem]">
|
||||
<h1 className="text-3xl font-bold mb-5">File Drop</h1>
|
||||
{[
|
||||
...new Set([
|
||||
...Object.keys(fileBundle || {}),
|
||||
...Object.keys(progressMessage),
|
||||
]),
|
||||
].map((name) => (
|
||||
<div className="mb-5 flex justify-between" key={name}>
|
||||
{name} {progressMessage[name]}
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!(name in (fileBundle || {}))}
|
||||
onClick={() => {
|
||||
const streamId = fileBundle?.meta.coValue.get(name);
|
||||
streamId &&
|
||||
readBlobFromBinaryStream(
|
||||
streamId,
|
||||
localNode,
|
||||
false,
|
||||
(progress) =>
|
||||
setProgressMessage((old) => ({
|
||||
...old,
|
||||
[name]: `Loading ${Math.round(
|
||||
progress * 100
|
||||
)}%`,
|
||||
}))
|
||||
).then((blob) => {
|
||||
if (!blob) return;
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{(!fileBundle || fileBundle.meta.group.myRole() === "admin") && (
|
||||
<Input type="file" onChange={onChange} multiple />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Walkthrough: Continue with ./3_NewProjectForm.tsx */
|
||||
39
examples/file-drop/src/basicComponents/SubmittableInput.tsx
Normal file
39
examples/file-drop/src/basicComponents/SubmittableInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
examples/file-drop/src/basicComponents/TitleAndLogo.tsx
Normal file
10
examples/file-drop/src/basicComponents/TitleAndLogo.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Toaster } from ".";
|
||||
|
||||
export function TitleAndLogo({name}: {name: string}) {
|
||||
return <>
|
||||
<div className="flex items-center gap-2 justify-center mt-5">
|
||||
<img src="jazz-logo.png" className="h-5" /> {name}
|
||||
</div>
|
||||
<Toaster />
|
||||
</>
|
||||
}
|
||||
17
examples/file-drop/src/basicComponents/index.ts
Normal file
17
examples/file-drop/src/basicComponents/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export { Button } from "./ui/button";
|
||||
export { Checkbox } from "./ui/checkbox";
|
||||
export { Input } from "./ui/input";
|
||||
export { Skeleton } from "./ui/skeleton";
|
||||
export { Toaster } from "./ui/toaster";
|
||||
export { useToast } from "./ui/use-toast";
|
||||
export { SubmittableInput } from "./SubmittableInput";
|
||||
export { TitleAndLogo } from "./TitleAndLogo";
|
||||
export { ThemeProvider } from "./themeProvider";
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "./ui/table";
|
||||
6
examples/file-drop/src/basicComponents/lib/utils.ts
Normal file
6
examples/file-drop/src/basicComponents/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
72
examples/file-drop/src/basicComponents/themeProvider.tsx
Normal file
72
examples/file-drop/src/basicComponents/themeProvider.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: string;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: string;
|
||||
setTheme: (theme: string) => void;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState(
|
||||
() => localStorage.getItem(storageKey) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: string) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
56
examples/file-drop/src/basicComponents/ui/button.tsx
Normal file
56
examples/file-drop/src/basicComponents/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
28
examples/file-drop/src/basicComponents/ui/checkbox.tsx
Normal file
28
examples/file-drop/src/basicComponents/ui/checkbox.tsx
Normal 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 }
|
||||
25
examples/file-drop/src/basicComponents/ui/input.tsx
Normal file
25
examples/file-drop/src/basicComponents/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
15
examples/file-drop/src/basicComponents/ui/skeleton.tsx
Normal file
15
examples/file-drop/src/basicComponents/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
114
examples/file-drop/src/basicComponents/ui/table.tsx
Normal file
114
examples/file-drop/src/basicComponents/ui/table.tsx
Normal 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,
|
||||
}
|
||||
127
examples/file-drop/src/basicComponents/ui/toast.tsx
Normal file
127
examples/file-drop/src/basicComponents/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
33
examples/file-drop/src/basicComponents/ui/toaster.tsx
Normal file
33
examples/file-drop/src/basicComponents/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/basicComponents/ui/toast"
|
||||
import { useToast } from "@/basicComponents/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
192
examples/file-drop/src/basicComponents/ui/use-toast.ts
Normal file
192
examples/file-drop/src/basicComponents/ui/use-toast.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/basicComponents/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
48
examples/file-drop/src/components/Auth.tsx
Normal file
48
examples/file-drop/src/components/Auth.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalAuthComponent } from "jazz-react-auth-local";
|
||||
|
||||
import { Input, Button } from "../basicComponents";
|
||||
|
||||
export const PrettyAuthUI: LocalAuthComponent = ({
|
||||
loading,
|
||||
logIn,
|
||||
signUp,
|
||||
}) => {
|
||||
const [username, setUsername] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center p-5">
|
||||
{loading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div className="w-72 flex flex-col gap-4">
|
||||
<form
|
||||
className="w-72 flex flex-col gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
signUp(username);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Display name"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="webauthn"
|
||||
className="text-base"
|
||||
/>
|
||||
<Button asChild>
|
||||
<Input
|
||||
type="submit"
|
||||
value="Sign Up as new account"
|
||||
/>
|
||||
</Button>
|
||||
</form>
|
||||
<Button onClick={logIn}>
|
||||
Log In with existing account
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
45
examples/file-drop/src/components/InviteButton.tsx
Normal file
45
examples/file-drop/src/components/InviteButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import QRCode from "qrcode";
|
||||
|
||||
import { useToast, Button } from "../basicComponents";
|
||||
import { CoValue } from "cojson";
|
||||
import { Resolved, createInviteLink } from "jazz-react";
|
||||
|
||||
export function InviteButton<T extends CoValue>({ value }: { value?: Resolved<T> }) {
|
||||
const [existingInviteLink, setExistingInviteLink] = useState<string>();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
value?.meta.group?.myRole() === "admin" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="py-0"
|
||||
disabled={!value.meta.group || !value.id}
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
let inviteLink = existingInviteLink;
|
||||
if (value.meta.group && value.id && !inviteLink) {
|
||||
inviteLink = createInviteLink(value, "writer");
|
||||
setExistingInviteLink(inviteLink);
|
||||
}
|
||||
if (inviteLink) {
|
||||
const qr = await QRCode.toDataURL(inviteLink, {
|
||||
errorCorrectionLevel: "L",
|
||||
});
|
||||
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||
toast({
|
||||
title: "Copied invite link to clipboard!",
|
||||
description: (
|
||||
<img src={qr} className="w-20 h-20" />
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
76
examples/file-drop/src/index.css
Normal file
76
examples/file-drop/src/index.css
Normal file
@@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
1
examples/file-drop/src/vite-env.d.ts
vendored
Normal file
1
examples/file-drop/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
76
examples/file-drop/tailwind.config.js
Normal file
76
examples/file-drop/tailwind.config.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
29
examples/file-drop/tsconfig.json
Normal file
29
examples/file-drop/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
examples/file-drop/tsconfig.node.json
Normal file
10
examples/file-drop/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
examples/file-drop/vite.config.ts
Normal file
16
examples/file-drop/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: false
|
||||
}
|
||||
})
|
||||
10
examples/pets/CHANGELOG.md
Normal file
10
examples/pets/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-browser-media-images@0.5.0
|
||||
- jazz-react@0.5.0
|
||||
- jazz-react-auth-local@0.4.16
|
||||
@@ -3,7 +3,7 @@ job "example-pets$BRANCH_SUFFIX" {
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 8
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
|
||||
@@ -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",
|
||||
|
||||
9
examples/todo/CHANGELOG.md
Normal file
9
examples/todo/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.5.0
|
||||
- jazz-react-auth-local@0.4.16
|
||||
@@ -3,7 +3,7 @@ job "example-todo$BRANCH_SUFFIX" {
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 8
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
26
examples/twit-stresstest/.gitignore
vendored
Normal 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
|
||||
13
examples/twit-stresstest/CHANGELOG.md
Normal file
13
examples/twit-stresstest/CHANGELOG.md
Normal 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
|
||||
104
examples/twit-stresstest/index.ts
Normal file
104
examples/twit-stresstest/index.ts
Normal 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();
|
||||
}
|
||||
17
examples/twit-stresstest/newAllTweets.ts
Normal file
17
examples/twit-stresstest/newAllTweets.ts
Normal 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);
|
||||
17
examples/twit-stresstest/package.json
Normal file
17
examples/twit-stresstest/package.json
Normal 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\""
|
||||
}
|
||||
}
|
||||
19
examples/twit-stresstest/tsconfig.json
Normal file
19
examples/twit-stresstest/tsconfig.json
Normal 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"],
|
||||
}
|
||||
14
examples/twit/CHANGELOG.md
Normal file
14
examples/twit/CHANGELOG.md
Normal 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
|
||||
@@ -3,7 +3,7 @@ job "twit$BRANCH_SUFFIX" {
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
count = 8
|
||||
count = 4
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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');
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
35
homepage/homepage-jazz-old/.gitignore
vendored
35
homepage/homepage-jazz-old/.gitignore
vendored
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const withNextra = require('nextra')({
|
||||
theme: 'nextra-theme-docs',
|
||||
themeConfig: './theme.config.jsx',
|
||||
mdxOptions: {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = withNextra()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# API docs
|
||||
@@ -1 +0,0 @@
|
||||
# Something
|
||||
@@ -1 +0,0 @@
|
||||
# Something else
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# Guides
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"gettingStarted": "Getting Started"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# Getting started
|
||||
@@ -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 — 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 “big ones” <small>(G‑Docs, Figma, Notion, Linear, …)</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…
|
||||
|
||||
<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** — so we're calling it “secure collaborative data.”
|
||||
|
||||
### 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
|
||||
@@ -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.
|
||||
@@ -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: []
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
}
|
||||
};
|
||||
@@ -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
7
homepage/homepage-jazz/.dockerignore
Normal file
7
homepage/homepage-jazz/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
||||
65
homepage/homepage-jazz/Dockerfile
Normal file
65
homepage/homepage-jazz/Dockerfile
Normal 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 |
@@ -75,6 +75,9 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
.overlay-close {
|
||||
background-color: "black";
|
||||
}
|
||||
}
|
||||
|
||||
pre.shiki {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 × 10 pages = 30,000 transactions
|
||||
- 30,000 transactions stored = ${30000 * pricePerTxStored} / mo
|
||||
- 3 × 30,000 transactions synced out = ${3 * 30000 * pricePerTxSyncedOut} one-time
|
||||
- 4 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 2h/day for a month
|
||||
- 4 users × 10 FPS × 10s/min × 60min/h * 2h/day × 30days = 1.44M transactions
|
||||
- 1.44M transactions stored = ${1440000 * pricePerTxStored} / mo = ${1440000 * pricePerTxStored / 4} / mo / user
|
||||
- 3 × 1.44M transactions synced out = ${(3 * 1440000 * pricePerTxSyncedOut).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time = ${(3 * 1440000 * pricePerTxSyncedOut / 4).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time / user
|
||||
- A livestreamer streaming video (1GB total) to 100 viewers (combined live & on-demand)
|
||||
- 1GB = 10,000 transactions (100KB each)
|
||||
- 10,000 transactions stored = ${10000 * pricePerTxStored} / mo (= ${10000 * 1000 * pricePerTxStored} per 1TB stored)
|
||||
- 100 × 10,000 transaction synced out = ${100 * 10000 * pricePerTxSyncedOut} one-time (= ${10000 * 1000 * pricePerTxSyncedOut} per 1TB egress)
|
||||
</div>
|
||||
|
||||
## Global Footprint
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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 — 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 data‑fetching</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 data‑fetching</GridFeature>
|
||||
<GridFeature icon={<UploadCloudIcon />}>Local & cloud persistence</GridFeature>
|
||||
<GridFeature icon={<PlaneIcon />}>Offline support & Quick reconnect</GridFeature>
|
||||
<GridFeature icon={<GaugeIcon />}>Instant UI updates & quick loads</GridFeature>
|
||||
</Grid>
|
||||
|
||||
<div className="-mx-[calc(min(0,(100vw-95rem)/2))]">
|
||||
### First impressions: 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 — 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
Reference in New Issue
Block a user