ribbit/examples/flask-collab/templates/index.html

175 lines
7.3 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Ribbit Collaboration Example</title>
<link rel="stylesheet" href="/static/ribbit/themes/ribbit-default/theme.css">
<style>
body { font-family: sans-serif; max-width: 800px; margin: 40px auto; }
#peers { padding: 8px; background: #f0f0f0; border-radius: 4px; margin-bottom: 10px; font-size: 14px; }
#peers .peer { display: inline-block; padding: 2px 8px; border-radius: 3px; margin-right: 4px; color: white; }
#status { font-size: 12px; color: #666; margin-bottom: 10px; }
#revisions { margin-top: 20px; }
#revisions button { margin: 2px; }
#ribbit { border: 1px solid #ccc; border-radius: 4px; padding: 20px; min-height: 200px; }
.ribbit-toolbar { background: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 4px; margin-bottom: 8px; }
.ribbit-toolbar ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 2px; align-items: center; }
.ribbit-toolbar button { padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; font-size: 12px; }
.ribbit-toolbar button:hover { background: #e8e8e8; }
.ribbit-toolbar button.active { background: #d0d0ff; border-color: #99f; }
.ribbit-toolbar button.disabled { opacity: 0.3; cursor: default; }
.ribbit-toolbar .spacer { width: 12px; }
.ribbit-dropdown { position: absolute; background: white; border: 1px solid #ccc; border-radius: 4px; padding: 4px; z-index: 10; }
.ribbit-dropdown button { display: block; width: 100%; text-align: left; margin: 1px 0; }
</style>
</head>
<body>
<h1>Ribbit Collaboration Example</h1>
<div id="peers">No peers connected</div>
<div id="status"></div>
<article id="ribbit">{{ content }}</article>
<div id="revisions">
<h3>Revisions</h3>
<div id="revision-list">Loading...</div>
</div>
<script src="/static/ribbit/ribbit.js"></script>
<script>
const userId = 'user-' + Math.random().toString(36).slice(2, 6);
const colors = ['#e74c3c', '#3498db', '#2ecc71', '#9b59b6', '#f39c12', '#1abc9c'];
const color = colors[Math.floor(Math.random() * colors.length)];
const ws = new WebSocket(`ws://${location.host}/ws`);
const transport = {
connect() {
ws.send(JSON.stringify({
type: 'join',
user: { userId, displayName: userId, color, status: 'active', lastActive: Date.now() },
}));
},
disconnect() {},
send(update) { if (ws.readyState === 1) ws.send(update); },
onReceive(callback) {
ws.addEventListener('message', (e) => {
if (e.data instanceof Blob) {
e.data.arrayBuffer().then(buf => callback(new Uint8Array(buf)));
}
});
},
async lock() {
const res = await fetch('/api/lock', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, displayName: userId }),
});
return res.ok;
},
unlock() { fetch('/api/lock', { method: 'DELETE' }); },
async forceLock() {
const res = await fetch('/api/lock/force', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, displayName: userId }),
});
return res.ok;
},
onLockChange(callback) {
ws.addEventListener('message', (e) => {
if (typeof e.data === 'string') {
const msg = JSON.parse(e.data);
if (msg.type === 'lock') callback(msg.holder);
}
});
},
};
const presence = {
send(info) {
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'presence', ...info }));
},
onUpdate(callback) {
ws.addEventListener('message', (e) => {
if (typeof e.data === 'string') {
const msg = JSON.parse(e.data);
if (msg.type === 'peers') callback(msg.peers);
}
});
},
};
const revisions = {
async list() {
return (await fetch('/api/revisions')).json();
},
async get(id) {
return (await fetch(`/api/revisions/${id}`)).json();
},
async create(content, metadata) {
const res = await fetch('/api/revisions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, ...metadata }),
});
return res.json();
},
};
const editor = new ribbit.Editor({
collaboration: {
transport,
presence,
revisions,
user: { userId, displayName: userId, color, status: 'active', lastActive: Date.now() },
},
on: {
peerChange({ peers }) {
const el = document.getElementById('peers');
if (peers.length === 0) {
el.innerHTML = 'No peers connected';
} else {
el.innerHTML = peers.map(p =>
`<span class="peer" style="background:${p.color || '#999'}">${p.displayName} (${p.status})</span>`
).join('');
}
},
lockChange({ holder }) {
const el = document.getElementById('status');
el.textContent = holder ? `🔒 Locked by ${holder.displayName}` : '';
},
remoteActivity({ count }) {
const el = document.getElementById('status');
el.textContent = `${count} remote change${count > 1 ? 's' : ''} while in source mode`;
},
save({ markdown }) {
revisions.create(markdown, { author: userId, summary: 'Manual save' }).then(refreshRevisions);
},
revisionCreated() {
refreshRevisions();
},
},
});
editor.run();
async function refreshRevisions() {
const list = await editor.listRevisions();
const el = document.getElementById('revision-list');
if (list.length === 0) {
el.innerHTML = '<em>No revisions yet. Click Save to create one.</em>';
} else {
el.innerHTML = list.map(r =>
`<button onclick="restore('${r.id}')">${r.timestamp} by ${r.author}${r.summary ? ': ' + r.summary : ''}</button>`
).join('<br>');
}
}
window.restore = async function(id) {
await editor.restoreRevision(id);
};
refreshRevisions();
</script>
</body>
</html>