fuzzyworld

north-star / commit

adding qr code and changing some styles

1909c7ef6d74522429a955f805062e86b5165bef · Pavel Safronov · 2026-03-15 09:29:25 -0700 · parents faf3a933731c

adding qr code and changing some styles
 src/app.js     | 88 +++++++++++++++++++---------------------------------------
 src/index.html | 16 +++++++----
 src/style.css  | 48 +++++++++++++++++++++++++++++---
 3 files changed, 82 insertions(+), 70 deletions(-)

diff --git a/src/app.js b/src/app.js
index c55079f..1eb135d 100644
--- a/src/app.js
+++ b/src/app.js
@@ -5,15 +5,16 @@ import {
     signInAnonymously,
 } from 'https://www.gstatic.com/firebasejs/12.10.0/firebase-auth.js';
 import { getDatabase, ref, set, onDisconnect } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-database.js";
+import QRCode from 'https://cdn.jsdelivr.net/npm/qrcode@1.5.4/+esm';
 
 const debugParagraph = document.getElementById('debug');
 const nameForm = document.getElementById('name-form');
 const selfNameInput = document.getElementById('self-name-input');
 const selfColorInput = document.getElementById('self-color-input');
 const roomCurrent = document.getElementById('room-current');
-const inviteRoomLink = document.getElementById('invite-room-link');
+const inviteRoomQr = document.getElementById('invite-room-qr');
+const inviteRoomQrStatus = document.getElementById('invite-room-qr-status');
 const calibrateHeadingLink = document.getElementById('calibrate-heading-link');
-const inviteRoomStatus = document.getElementById('invite-room-status');
 const createRoomLink = document.getElementById('create-room-link');
 const participantsPanel = document.getElementById('participants-panel');
 const participantsToggle = document.getElementById('participants-toggle');
@@ -191,14 +192,35 @@ if (roomCurrent) {
     roomCurrent.textContent = `You are currently in room ${roomId}`;
 }
 
-if (inviteRoomLink) {
-    const keyFragment = roomKeyBase64 ? `&key=${encodeURIComponent(roomKeyBase64)}` : '';
-    inviteRoomLink.href = `${BASE_ROOM_URL}#room=${encodeURIComponent(roomId)}${keyFragment}`;
-}
+const keyFragment = roomKeyBase64 ? `&key=${encodeURIComponent(roomKeyBase64)}` : '';
+const inviteRoomUrl = `${BASE_ROOM_URL}#room=${encodeURIComponent(roomId)}${keyFragment}`;
 if (createRoomLink) {
     createRoomLink.href = BASE_ROOM_URL;
 }
 
+if (inviteRoomQr) {
+    try {
+        const qrDataUrl = await QRCode.toDataURL(inviteRoomUrl, {
+            width: 280,
+            margin: 1,
+            errorCorrectionLevel: 'M',
+            color: {
+                dark: '#0f172a',
+                light: '#ffffff'
+            }
+        });
+        inviteRoomQr.src = qrDataUrl;
+        inviteRoomQr.alt = `Scan to join room ${roomId}`;
+        if (inviteRoomQrStatus) inviteRoomQrStatus.textContent = 'Scan to join this room';
+    } catch (err) {
+        console.error('Failed to generate invite QR code', err);
+        if (inviteRoomQrStatus) {
+            inviteRoomQrStatus.textContent = 'Failed to create QR';
+            inviteRoomQrStatus.classList.add('error');
+        }
+    }
+}
+
 const requiresExplicitCompassPermission =
     typeof DeviceOrientationEvent !== 'undefined' &&
     typeof DeviceOrientationEvent.requestPermission === 'function';
@@ -366,60 +388,6 @@ const requestCompassPermission = async () => {
 updateCompassButtonState();
 scheduleCompassProbe();
 
-let inviteStatusTimer = null;
-const setInviteStatus = (message, isError = false) => {
-    if (!inviteRoomStatus) return;
-
-    inviteRoomStatus.textContent = message;
-    inviteRoomStatus.classList.toggle('error', isError);
-
-    if (inviteStatusTimer) {
-        clearTimeout(inviteStatusTimer);
-    }
-
-    inviteStatusTimer = setTimeout(() => {
-        inviteRoomStatus.textContent = '';
-        inviteRoomStatus.classList.remove('error');
-    }, 1800);
-};
-
-const copyTextToClipboard = async (text) => {
-    if (navigator.clipboard && window.isSecureContext) {
-        await navigator.clipboard.writeText(text);
-        return;
-    }
-
-    const tempTextArea = document.createElement('textarea');
-    tempTextArea.value = text;
-    tempTextArea.setAttribute('readonly', '');
-    tempTextArea.style.position = 'fixed';
-    tempTextArea.style.left = '-9999px';
-    document.body.appendChild(tempTextArea);
-    tempTextArea.select();
-
-    const copied = document.execCommand('copy');
-    document.body.removeChild(tempTextArea);
-
-    if (!copied) {
-        throw new Error('Copy command failed');
-    }
-};
-
-if (inviteRoomLink) {
-    inviteRoomLink.addEventListener('click', async (event) => {
-        event.preventDefault();
-
-        try {
-            const inviteText = `Join me on North Star — live arrows pointing to each other, no account needed:\n${inviteRoomLink.href}`;
-            await copyTextToClipboard(inviteText);
-            setInviteStatus('Copied!');
-        } catch (err) {
-            console.error('Failed to copy invite URL', err);
-            setInviteStatus('Copy failed', true);
-        }
-    });
-}
-
 if (calibrateHeadingLink) {
     calibrateHeadingLink.addEventListener('click', async (event) => {
         event.preventDefault();
diff --git a/src/index.html b/src/index.html
index f8ba3ec..655be75 100644
--- a/src/index.html
+++ b/src/index.html
@@ -26,13 +26,11 @@
 <body>
     <p class="app-title"><a href="https://gitlab.com/PavelSafronov/north-star" target="_blank"
             rel="noopener noreferrer">North Star</a></p>
-    <p id="room-current" class="room-current"></p>
-    <p class="room-links">
-        <a id="invite-room-link" href="#">Invite to Room</a>
-        <span aria-hidden="true">·</span>
+    <div class="room-header">
+        <p id="room-current" class="room-current"></p>
         <a id="create-room-link" href="https://pavelsafronov.gitlab.io/north-star/" target="_blank"
             rel="noopener noreferrer">Create New Room</a>
-    </p>
+    </div>
     <form id="name-form" class="name-form" autocomplete="off">
         <input id="self-name-input" name="selfName" type="text" maxlength="32" enterkeyhint="done" />
         <input id="self-color-input" name="selfColor" type="color" />
@@ -48,8 +46,14 @@
         </div>
         <aside id="participants-panel" class="participants-panel" aria-label="Participants"></aside>
     </div>
+    <div class="room-links">
+        <div class="room-qr-card" aria-label="Room invite QR code">
+            <img id="invite-room-qr" class="room-qr" alt="Room invite QR code" />
+            <p id="invite-room-qr-status" class="room-link-status"></p>
+        </div>
+    </div>
     <div>
-        <p id="debug"></p>
+        <p id="debug" style="display: none;"></p>
     </div>
     <script src="app.js" type="module"></script>
 </body>
diff --git a/src/style.css b/src/style.css
index 36112e7..69c7ab3 100644
--- a/src/style.css
+++ b/src/style.css
@@ -43,6 +43,25 @@ a {
     text-decoration: none;
 }
 
+.room-header {
+    margin: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-wrap: wrap;
+    gap: 10px;
+    position: relative;
+    z-index: 2;
+    text-align: center;
+}
+
+.room-header a {
+    font-size: clamp(12px, 1.8vmin, 16px);
+    font-weight: 600;
+    text-decoration: underline;
+    text-underline-offset: 0.15em;
+}
+
 .room-current {
     margin: 0;
     font-size: clamp(12px, 1.8vmin, 16px);
@@ -57,10 +76,10 @@ a {
 .room-links {
     margin: 0;
     display: flex;
+    flex-direction: column;
     align-items: center;
     justify-content: center;
-    flex-wrap: wrap;
-    gap: 10px;
+    gap: 8px;
     font-size: clamp(13px, 2vmin, 18px);
     font-weight: 600;
     position: relative;
@@ -68,11 +87,31 @@ a {
     text-align: center;
 }
 
-.room-links a {
+.room-links>a {
     text-decoration: underline;
     text-underline-offset: 0.15em;
 }
 
+.room-qr-card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 6px;
+    padding: 8px;
+    border: 1px solid #cbd5e1;
+    border-radius: 12px;
+    background: rgba(255, 255, 255, 0.96);
+    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
+}
+
+.room-qr {
+    width: 140px;
+    height: 140px;
+    display: block;
+    background: #ffffff;
+    border-radius: 8px;
+}
+
 .room-links span {
     color: rgba(51, 65, 85, 0.8);
 }
@@ -80,8 +119,9 @@ a {
 .room-link-status {
     min-width: 58px;
     color: #166534;
-    font-size: 0.9em;
+    font-size: 0.78em;
     text-shadow: none;
+    margin: 0;
 }
 
 .room-link-status.error {