initial version

main
Niko PLP 3 days ago
commit 2f7ea4a99e
  1. 24
      .gitignore
  2. 10
      README.md
  3. 28
      eslint.config.js
  4. 12
      index.html
  5. 7990
      package-lock.json
  6. 43
      package.json
  7. 6
      postcss.config.js
  8. 1
      public/vite.svg
  9. 53
      src/App.css
  10. 28
      src/App.tsx
  11. 51
      src/Doc.tsx
  12. 44
      src/Header.tsx
  13. 26
      src/Home.tsx
  14. 36
      src/base64url.js
  15. 3
      src/index.css
  16. 10
      src/main.tsx
  17. 93
      src/provider.js
  18. 6
      src/reactMethods.ts
  19. 226
      src/styles.css
  20. 1
      src/vite-env.d.ts
  21. 12
      tailwind.config.js
  22. 26
      tsconfig.app.json
  23. 7
      tsconfig.json
  24. 25
      tsconfig.node.json
  25. 7
      vite.config.ts

24
.gitignore vendored

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

@ -0,0 +1,10 @@
# RBlockNote and NextGraph integration demo
```
npm i
npm run dev
```
you need to create a wallet at https://nextgraph.eu
Standalone webapps do not work on Safari (yet).

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlockNote + NextGraph</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

7990
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,43 @@
{
"name": "blocknote",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@blocknote/core": "^0.31.1",
"@blocknote/mantine": "^0.31.1",
"@blocknote/react": "^0.31.1",
"@heroicons/react": "^2.2.0",
"lib0": "^0.2.108",
"nextgraph-react": "^0.1.1-alpha.3",
"nextgraphweb": "^0.1.1-alpha.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.1",
"react-router-dom": "^7.6.1",
"tailwindcss": "^4.1.8",
"yjs": "^13.6.27"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,53 @@
.fullscreen {
top: 40px;
position:relative;
}
.doc {
height: 100vh;
}
.centered {
/*max-width: 1280px;*/
margin: 0 auto;
padding: 0rem;
text-align: center;
width: fit-content;
}
.contact {
width: 300px;
height: 300px;
background-color: #f6f6f6;
position: relative;
overflow-wrap: anywhere;
}
.name {
padding: 5px;
height: 35px;
overflow: hidden;
background-color: #e0e0e0d0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.email-logo {
}
.email {
padding: 5px;
overflow-wrap: anywhere;
}
input {
margin: 5px;
}
.button {
background-color:rgb(73, 114, 165);
color:white;
cursor:pointer;
}

@ -0,0 +1,28 @@
import React, { FunctionComponent } from 'react';
import { Header } from './Header';
import { Doc } from './Doc';
import { Home } from './Home';
import { NextGraphAuthMethod } from './reactMethods';
import { BrowserRouter, Routes, Route } from "react-router";
import './App.css'
import "./styles.css";
const App: FunctionComponent = () => {
return (
<div className="App">
<NextGraphAuthMethod>
<Header />
<BrowserRouter>
<Routes>
<Route index element={<Home />} />
<Route path=":nuri" element={<Doc />} />
</Routes>
</BrowserRouter>
</NextGraphAuthMethod>
</div>
)
}
export default App

@ -0,0 +1,51 @@
import "@blocknote/core/fonts/inter.css";
import { default as React, FunctionComponent, useMemo } from "react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useCreateBlockNote } from "@blocknote/react";
import * as Y from "yjs";
import { NextGraphProvider } from "./provider";
import { useNextGraphAuth } from './reactMethods';
import { useParams } from "react-router";
export const Doc: FunctionComponent = () => {
const { session } = useNextGraphAuth();
const params = useParams();
const [doc, provider] = useMemo(() => {
const doc = new Y.Doc();
const provider = new NextGraphProvider(params.nuri, doc, session);
return [doc, provider];
}, [params, session]);
const editor = useCreateBlockNote({
collaboration: {
// The Yjs Provider responsible for transporting updates:
provider,
// Where to store BlockNote data in the Y.Doc:
fragment: doc.getXmlFragment('document-store'),
// Information (name and color) for this user:
user: {
name: "My Username",
color: "#ff0000",
},
// When to show user labels on the collaboration cursor. Set by default to
// "activity" (show when the cursor moves), but can also be set to "always".
showCursorLabels: "activity"
},
}, [params, session])
// Creates a new editor instance.
if (!session.sessionId) return <></>;
// Renders the editor instance using a React component.
return (
<div className="doc">
<div className="fullscreen">
<BlockNoteView editor={editor} />
</div>
</div>
);
}

@ -0,0 +1,44 @@
import { FunctionComponent } from "react";
import { useNextGraphAuth } from "./reactMethods";
export const Header: FunctionComponent = () => {
const { session, login, logout } = useNextGraphAuth();
return (
<div>
{session.sessionId ? (
// If the session is logged in
<div className="p-1 text-white text-center fixed top-0 left-0 right-0" style={{zIndex:1000, height:'36px', backgroundColor:'rgb(73, 114, 165)'}}>
You are logged in.
{/* <span className="font-bold clickable" onClick={logout}> Log out</span> */}
</div>
) : (
// If the session is not logged in
<>
<h1 className="text-2xl text-center mb-10">Welcome to BlockNote + NextGraph demo</h1>
<div className="text-center text-xl p-1 text-white fixed top-0 left-0 right-0" style={{zIndex:1000, height:'36px', backgroundColor:'rgb(73, 114, 165)'}}>
Please <span className="font-bold clickable" onClick={login}> Log in</span>
</div>
<div className="text-center max-w-6xl lg:px-8 mx-auto px-4 text-blue-800">
<svg onClick={login}
onKeyUp={login} className="cursor-pointer mt-10 h-16 w-16 mx-auto" data-slot="icon" fill="none" strokeWidth="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15M12 9l3 3m0 0-3 3m3-3H2.25"></path>
</svg>
<button
onClick={login}
onKeyUp={login}
className="select-none ml-0 mt-2 mb-10 text-white bg-blue-800 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-500/50 rounded-lg text-base p-2 text-center inline-flex items-center dark:focus:ring-primary-700/55"
>
Please Log in
</button>
</div>
</>
)}
</div>
);
};

@ -0,0 +1,26 @@
import { FunctionComponent } from "react";
import { useNextGraphAuth } from "./reactMethods";
import { DocumentPlusIcon } from '@heroicons/react/24/outline'
import { useNavigate } from "react-router-dom";
export const Home: FunctionComponent = () => {
const { session } = useNextGraphAuth();
const navigate = useNavigate();
const create = () => {
session.ng.doc_create(session.sessionId, "YXml","post:blocknote","store").then((nuri) => {
console.log("new doc created",nuri)
navigate("/"+nuri);
});
};
if (!session.sessionId) return <></>;
return <>
<div className="centered">
<div className="flex flex-wrap justify-center gap-5 mt-10 mb-10">
<button onClick={create} className="button"><DocumentPlusIcon className="size-7 inline"/> Create Document</button>
</div>
</div>
</>;
};

@ -0,0 +1,36 @@
/*
* Base64URL-ArrayBuffer
* https://github.com/herrjemand/Base64URL-ArrayBuffer
*
* Copyright (c) 2017 Yuriy Ackermann <ackermann.yuriy@gmail.com>
* Copyright (c) 2012 Niklas von Hertzen
* Licensed under the MIT license.
*
*/
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
// Use a lookup table to find the index.
var lookup = new Uint8Array(256);
for (var i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
export const encode = function(arraybuffer) {
var bytes = new Uint8Array(arraybuffer),
i, len = bytes.length, base64 = "";
for (i = 0; i < len; i+=3) {
base64 += chars[bytes[i] >> 2];
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += chars[bytes[i + 2] & 63];
}
if ((len % 3) === 2) {
base64 = base64.substring(0, base64.length - 1);
} else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2);
}
return base64;
};

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,10 @@
//import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('app')!).render(
// <StrictMode>
<App />
// </StrictMode>,
)

@ -0,0 +1,93 @@
import { ObservableV2 } from 'lib0/observable'
import { encode } from "./base64url";
import * as Y from "yjs";
const digest_to_string = function(digest) {
let copy = [...digest.Blake3Digest32];
copy.reverse();
copy.push(0);
let buffer = Uint8Array.from(copy);
return encode(buffer.buffer);
};
export class NextGraphProvider extends ObservableV2 {
/**
* @param {string} nuri
* @param {Y.Doc} doc
* @param {Object} session
**/
constructor (nuri, doc, session) {
super()
if (!session.ng) return;
this.nuri = nuri
this.doc = doc
this.session = session
this.heads = [];
this.shouldConnect = false
/**
* Listens to Yjs updates and sends them to NextGraph
* @param {Uint8Array} update
* @param {any} origin
*/
this._updateHandler = async (update, origin) => {
if (!origin.local) {
try {
await this.session.ng.discrete_update(this.session.sessionId, update, this.heads, "YXml", this.nuri);
} catch (e){
console.log(e);
}
}
}
this.doc.on('update', this._updateHandler)
this.destroy = this.destroy.bind(this)
this.doc.on('destroy', this.destroy)
this.connect()
}
destroy () {
this.disconnect()
//this.awareness.off('update', this._awarenessUpdateHandler)
this.doc.off('update', this._updateHandler)
super.destroy()
}
disconnect () {
this.shouldConnect = false
// TODO unsub
}
connect () {
console.log("connecting to doc",this.nuri.substring(0,53))
if (this.shouldConnect) return;
this.shouldConnect = true
this.session.ng.doc_subscribe(this.nuri.substring(0,53), this.session.sessionId,
(response) => {
//console.log("GOT APP RESPONSE", response);
if (response.V0.TabInfo) {
} else if (response.V0.State) {
if (response.V0.State.discrete) {
Y.applyUpdate(this.doc, response.V0.State.discrete.YXml, {local:true})
}
for (const head of response.V0.State.heads) {
let commitId = digest_to_string(head);
this.heads.push(commitId);
}
} else if (response.V0.Patch) {
if (response.V0.Patch.discrete) {
Y.applyUpdate(this.doc, response.V0.Patch.discrete.YXml, {local:true})
}
let i = this.heads.length;
while (i--) {
if (response.V0.Patch.commit_info.past.includes(this.heads[i])) {
this.heads.splice(i, 1);
}
}
this.heads.push(response.V0.Patch.commit_id);
}
}
); // TODO .then keep unsub
}
}

@ -0,0 +1,6 @@
import { createNextGraphAuthMethod } from "nextgraph-react";
const methods = createNextGraphAuthMethod();
export const { NextGraphAuthMethod, useNextGraphAuth } = methods;

@ -0,0 +1,226 @@
/*
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
*/
/** To format paths, like Settings > Wallet > Generate Wallet QR */
.path {
font-family: monospace;
background-color: rgba(73, 114, 165, 0.1);
}
/* .splash-loaded {
display: none;
} */
.toggle * {
cursor: pointer;
}
.error-popover h3 {
text-align: center;
color: rgb(200 30 30);
}
.error-popover > div:first-child {
background-color: rgb(200 30 30);
}
.error-popover > div:first-child > h3 {
color: white;
}
.logo {
padding: 1.5em;
will-change: filter;
transition: 0.75s;
padding-bottom: 1em;
}
@keyframes pulse-logo-color {
0%,
100% {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
50% {
/* Mid-transition color */
stroke: #bbb;
fill: #bbb;
}
}
.logo-pulse path {
animation: pulse-logo-color 2s infinite;
animation-timing-function: cubic-bezier(0.65, 0.01, 0.59, 0.83);
}
.logo-gray path {
fill: #bbb;
stroke: #bbb;
}
.logo-blue path {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
.jse-absolute-popup-content {
left: 0 !important;
}
.container3 {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.container3 aside {
width: 20rem !important;
}
div[role="alert"] div {
display: block;
}
.spinner-overlay button {
display: none;
}
.choice-button {
min-width: 305px;
}
.clickable {
cursor: pointer;
}
.row {
display: flex;
justify-content: center;
}
.deactivated-menu > svg {
color: rgb(156 163 175) !important;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: white;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 305px;
min-height: 100vh;
}
#app {
width: 100%;
}
/* #app {
/*max-width: 1280px;
margin: 0 auto;
padding: 0rem;
text-align: center;
} */
/* .container2 {
padding-top: 10vh;
} */
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
.toast {
left: 50%;
transform: translateX(-50%);
z-index: 49;
cursor: pointer;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
/* input,
button {
outline: none;
} */
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
/* @media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
} */

1
src/vite-env.d.ts vendored

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

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,jsx,ts,tsx}"
],
theme: {
extend: {},
},
plugins: [],
}

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
Loading…
Cancel
Save