north-star / commit
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 {