Skip to content

Commit 38b0f02

Browse files
committed
Add Google Drive and OneDrive sync pages with login buttons and sync toggle header
- Refactor database and UI logic into shared modules (shared-database.ts, shared-ui.ts) - Add Google Drive sync page using rxdb replication-google-drive plugin with OAuth login - Add Microsoft OneDrive sync page using rxdb replication-microsoft-onedrive plugin with OAuth login - Add sync method navigation header to all pages (WebRTC, Google Drive, OneDrive) - Update webpack config for multi-page output with separate entry points https://claude.ai/code/session_01WoskKBdRqYXaUNYptTsTA8
1 parent f30fd0e commit 38b0f02

10 files changed

Lines changed: 565 additions & 137 deletions

src/google-drive.html

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<!doctype html>
2+
<html
3+
lang="en"
4+
data-framework="rxdb"
5+
>
6+
7+
<head>
8+
<meta charset="utf-8">
9+
<title>RxDB Quickstart - Google Drive Sync</title>
10+
</head>
11+
12+
<body>
13+
14+
<span id="forkongithub">
15+
<a
16+
href="https://github.com/pubkey/rxdb-quickstart"
17+
target="_blank"
18+
>Fork me on GitHub</a>
19+
</span>
20+
21+
<nav class="sync-nav">
22+
<span class="sync-nav-label">Sync method:</span>
23+
<a href="index.html" class="sync-nav-link">WebRTC</a>
24+
<a href="google-drive.html" class="sync-nav-link active">Google Drive</a>
25+
<a href="onedrive.html" class="sync-nav-link">OneDrive</a>
26+
</nav>
27+
28+
<section class="todoapp">
29+
<header class="header">
30+
<h1>Google Drive todos</h1>
31+
<p class="description">
32+
This is a <a
33+
href="https://rxdb.info/offline-first.html"
34+
target="_blank"
35+
>local first</a> todo app that stores data locally with <a
36+
href="https://rxdb.info/"
37+
target="_blank"
38+
>RxDB</a> and
39+
replicates it to <a
40+
href="https://rxdb.info/replication-google-drive.html"
41+
target="_blank"
42+
>Google Drive</a> so your data syncs across all your devices.
43+
</p>
44+
<div class="cloud-login">
45+
<button id="google-login-btn" class="login-btn login-btn-google">
46+
Sign in with Google
47+
</button>
48+
<span id="login-status" class="login-status">Not connected</span>
49+
</div>
50+
<hr>
51+
<input
52+
id="insert-todo"
53+
class="new-todo"
54+
placeholder="What needs to be done? (Enter to submit)"
55+
autofocus
56+
>
57+
</header>
58+
<section class="main">
59+
<input
60+
id="toggle-all"
61+
class="toggle-all"
62+
type="checkbox"
63+
>
64+
<label for="toggle-all">Mark all as complete</label>
65+
<ul
66+
class="todo-list"
67+
id="todo-list"
68+
>
69+
70+
</ul>
71+
<footer class="footer">
72+
<span class="todo-count"></span>
73+
<ul class="filters">
74+
</ul>
75+
<button
76+
class="clear-completed"
77+
id="clear-completed"
78+
>Clear completed</button>
79+
</footer>
80+
</section>
81+
</section>
82+
<footer class="info">
83+
<p>Written by <a
84+
href="https://github.com/pubkey"
85+
target="_blank"
86+
>Daniel</a></p>
87+
<p><a href="https://rxdb.info">Powered by RxDB</a></p>
88+
</footer>
89+
</body>
90+
91+
</html>

src/google-drive.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { replicateGoogleDrive } from 'rxdb/plugins/replication-google-drive';
2+
import { createTodoDatabase, TodoDocType } from './shared-database.js';
3+
import { initTodoUI, getById } from './shared-ui.js';
4+
import './style.css';
5+
6+
const GOOGLE_CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID';
7+
const SCOPES = 'https://www.googleapis.com/auth/drive.file';
8+
9+
let authToken: string | null = null;
10+
11+
function getTokenFromHash(): string | null {
12+
const hash = window.location.hash.substring(1);
13+
const params = new URLSearchParams(hash);
14+
return params.get('access_token');
15+
}
16+
17+
function loginWithGoogle() {
18+
const redirectUri = window.location.origin + window.location.pathname;
19+
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth' +
20+
'?client_id=' + encodeURIComponent(GOOGLE_CLIENT_ID) +
21+
'&redirect_uri=' + encodeURIComponent(redirectUri) +
22+
'&response_type=token' +
23+
'&scope=' + encodeURIComponent(SCOPES);
24+
window.location.href = authUrl;
25+
}
26+
27+
function updateLoginUI(loggedIn: boolean) {
28+
const $loginBtn = getById('google-login-btn');
29+
const $status = getById('login-status');
30+
if (loggedIn) {
31+
$loginBtn.style.display = 'none';
32+
$status.textContent = 'Connected to Google Drive';
33+
$status.style.color = '#4CAF50';
34+
} else {
35+
$loginBtn.style.display = 'inline-block';
36+
$status.textContent = 'Not connected';
37+
$status.style.color = '#999';
38+
}
39+
}
40+
41+
(async () => {
42+
// check for OAuth token in URL hash
43+
authToken = getTokenFromHash();
44+
if (authToken) {
45+
// clean the hash so it does not interfere
46+
history.replaceState(null, '', window.location.pathname + window.location.search);
47+
}
48+
49+
const $loginBtn = getById('google-login-btn');
50+
$loginBtn.onclick = () => loginWithGoogle();
51+
52+
updateLoginUI(!!authToken);
53+
54+
const database = await createTodoDatabase('-gdrive');
55+
initTodoUI(database);
56+
57+
if (authToken) {
58+
const replicationState = await replicateGoogleDrive<TodoDocType>({
59+
collection: database.todos,
60+
replicationIdentifier: 'google-drive-todos',
61+
googleDrive: {
62+
oauthClientId: GOOGLE_CLIENT_ID,
63+
authToken: authToken,
64+
folderPath: 'rxdb-quickstart/todos'
65+
},
66+
pull: {},
67+
push: {},
68+
live: true
69+
});
70+
replicationState.error$.subscribe((err: any) => {
71+
console.log('Google Drive replication error:');
72+
console.dir(err);
73+
});
74+
}
75+
})();

src/index.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<head>
88
<meta charset="utf-8">
9-
<title>RxDB Quickstart • TodoMVC</title>
9+
<title>RxDB Quickstart - WebRTC Sync</title>
1010
</head>
1111

1212
<body>
@@ -18,6 +18,13 @@
1818
>Fork me on GitHub</a>
1919
</span>
2020

21+
<nav class="sync-nav">
22+
<span class="sync-nav-label">Sync method:</span>
23+
<a href="index.html" class="sync-nav-link active">WebRTC</a>
24+
<a href="google-drive.html" class="sync-nav-link">Google Drive</a>
25+
<a href="onedrive.html" class="sync-nav-link">OneDrive</a>
26+
</nav>
27+
2128
<section class="todoapp">
2229
<header class="header">
2330
<h1>p2p todos</h1>
@@ -72,12 +79,6 @@ <h1>p2p todos</h1>
7279
<footer class="footer">
7380
<span class="todo-count"></span>
7481
<ul class="filters">
75-
<!-- <li>
76-
<a class="selected">All</a>
77-
</li> -->
78-
<!-- <li>
79-
<a>Completed</a>
80-
</li> -->
8182
</ul>
8283
<button
8384
class="clear-completed"
@@ -87,7 +88,6 @@ <h1>p2p todos</h1>
8788
</section>
8889
</section>
8990
<footer class="info">
90-
<!-- <p>Double-click to edit a todo</p> -->
9191
<p>Written by <a
9292
href="https://github.com/pubkey"
9393
target="_blank"

src/index.ts

Lines changed: 30 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,40 @@
1-
import {
2-
ensureNotFalsy,
3-
randomToken
4-
} from 'rxdb/plugins/core';
5-
import {
6-
RxTodoDocument,
7-
databasePromise
8-
} from './database.js';
1+
import { randomToken, defaultHashSha256 } from 'rxdb/plugins/core';
2+
import { replicateWebRTC, getConnectionHandlerSimplePeer, SimplePeer } from 'rxdb/plugins/replication-webrtc';
3+
import { createTodoDatabase, TodoDocType } from './shared-database.js';
4+
import { initTodoUI, getById } from './shared-ui.js';
95
import './style.css';
106

117
(async () => {
12-
const database = await databasePromise;
8+
// ensure roomId exists
9+
const roomId = window.location.hash;
10+
if (!roomId || roomId.length < 5) {
11+
window.location.hash = 'room-' + randomToken(10);
12+
window.location.reload();
13+
}
14+
const roomHash = await defaultHashSha256(roomId);
15+
16+
const database = await createTodoDatabase('-' + roomHash.substring(0, 10));
1317

1418
// update url in description text
1519
getById('copy-url').innerHTML = window.location.href;
1620

17-
// render reactive todo list
18-
const $todoList = getById('todo-list');
19-
database.todos.find({
20-
sort: [
21-
{ state: 'desc' },
22-
{ lastChange: 'desc' }
23-
]
24-
}).$.subscribe(todos => {
25-
$todoList.innerHTML = '';
26-
todos.forEach(todo => $todoList.append(getHtmlByTodo(todo)));
27-
});
28-
29-
// event: add todo
30-
const $insertInput = getById<HTMLInputElement>('insert-todo');
31-
const addTodo = async () => {
32-
if ($insertInput.value.length < 1) { return; }
33-
await database.todos.insert({
34-
id: randomToken(10),
35-
name: $insertInput.value,
36-
state: 'open',
37-
lastChange: Date.now()
21+
// setup WebRTC replication
22+
replicateWebRTC<TodoDocType, SimplePeer>({
23+
collection: database.todos,
24+
connectionHandlerCreator: getConnectionHandlerSimplePeer({}),
25+
topic: roomHash.substring(0, 10),
26+
pull: {},
27+
push: {},
28+
}).then(replicationState => {
29+
replicationState.error$.subscribe((err: any) => {
30+
console.log('replication error:');
31+
console.dir(err);
3832
});
39-
$insertInput.value = '';
40-
};
41-
$insertInput.onkeydown = async (event) => {
42-
if (isEnterEvent(event)) { addTodo(); }
43-
}
44-
$insertInput.onblur = () => addTodo();
33+
replicationState.peerStates$.subscribe(s => {
34+
console.log('new peer states:');
35+
console.dir(s);
36+
});
37+
});
4538

46-
// event: clear completed
47-
getById('clear-completed').onclick = () => database.todos.find({ selector: { state: 'done' } }).remove();
39+
initTodoUI(database);
4840
})();
49-
50-
function getById<T = HTMLElement>(id: string): T { return ensureNotFalsy(document.getElementById(id)) as any; }
51-
const escapeForHTML = (s: string) => s.replace(/[&<]/g, c => c === '&' ? '&amp;' : '&lt;');
52-
const isEnterEvent = (ev: KeyboardEvent) => ev.code === 'Enter' || ev.keyCode === 13;
53-
function getHtmlByTodo(todo: RxTodoDocument): HTMLLIElement {
54-
const $liElement = document.createElement('li');
55-
const $viewDiv = document.createElement('div');
56-
const $checkbox = document.createElement('input');
57-
const $label = document.createElement('label');
58-
const $deleteButton = document.createElement('button');
59-
$liElement.append($viewDiv);
60-
$viewDiv.append($checkbox);
61-
$viewDiv.append($label);
62-
$viewDiv.append($deleteButton);
63-
64-
// event: toggle todo state
65-
$checkbox.onclick = () => todo.incrementalPatch({ state: todo.state === 'done' ? 'open' : 'done' });
66-
$checkbox.type = 'checkbox';
67-
$checkbox.classList.add('toggle');
68-
69-
// event: change todo name
70-
$label.contentEditable = 'true';
71-
const updateName = async () => {
72-
let newName = $label.innerText || $label.textContent as string;
73-
newName = newName.replace(/<br>/g, '').replace(/\&nbsp;/g, ' ').trim();
74-
if (newName !== todo.name) {
75-
await todo.incrementalPatch({ name: newName });
76-
}
77-
}
78-
$label.onblur = () => updateName();
79-
$label.onkeyup = async (ev) => {
80-
if (isEnterEvent(ev)) {
81-
updateName();
82-
}
83-
}
84-
$label.innerHTML = escapeForHTML(todo.name);
85-
86-
// event: delete todo
87-
$deleteButton.classList.add('destroy');
88-
$deleteButton.onclick = () => todo.remove();
89-
90-
if (todo.state === 'done') {
91-
$liElement.classList.add('completed');
92-
$checkbox.checked = true;
93-
}
94-
95-
return $liElement;
96-
}

0 commit comments

Comments
 (0)