From a5b182ef31021aae3dcac8d47e98d44d59c3f5be Mon Sep 17 00:00:00 2001 From: ShubhamSPawde Date: Thu, 8 May 2025 03:11:36 +0530 Subject: [PATCH 1/6] feat: add QR code for room join --- .../.cursor/rules/posthog-integration.mdc | 29 -------------- apps/client/src/components/QRCodeModal.tsx | 25 ++++++++++++ apps/client/src/components/room/TopBar.tsx | 40 +++++++++++++------ bun.lock | 5 +++ package.json | 1 + 5 files changed, 59 insertions(+), 41 deletions(-) delete mode 100644 apps/client/.cursor/rules/posthog-integration.mdc create mode 100644 apps/client/src/components/QRCodeModal.tsx diff --git a/apps/client/.cursor/rules/posthog-integration.mdc b/apps/client/.cursor/rules/posthog-integration.mdc deleted file mode 100644 index f6cbda8f..00000000 --- a/apps/client/.cursor/rules/posthog-integration.mdc +++ /dev/null @@ -1,29 +0,0 @@ ---- -description: apply when interacting with PostHog/analytics tasks -globs: -alwaysApply: true ---- - -Never hallucinate an API key. Instead, always use the API key populated in the .env file. - -# Feature flags - -A given feature flag should be used in as few places as possible. Do not increase the risk of undefined behavior by scattering the same feature flag across multiple areas of code. If the same feature flag needs to be introduced at multiple callsites, flag this for the developer to inspect carefully. - -If a job requires creating new feature flag names, make them as clear and descriptive as possible. - -If using TypeScript, use an enum to store flag names. If using JavaScript, store flag names as strings to an object declared as a constant, to simulate an enum. Use a consistent naming convention for this storage. enum/const object members should be written UPPERCASE_WITH_UNDERSCORE. - -Gate flag-dependent code on a check that verifies the flag's values are valid and expected. - -# Custom properties - -If a custom property for a person or event is at any point referenced in two or more files or two or more callsites in the same file, use an enum or const object, as above in feature flags. - -# Naming - -Before creating any new event or property names, consult with the developer for any existing naming convention. Consistency in naming is essential, and additional context may exist outside this project. Similarly, be careful about any changes to existing event and property names, as this may break reporting and distort data for the project. - - - - diff --git a/apps/client/src/components/QRCodeModal.tsx b/apps/client/src/components/QRCodeModal.tsx new file mode 100644 index 00000000..190c30fa --- /dev/null +++ b/apps/client/src/components/QRCodeModal.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import QRCode from 'react-qr-code'; +import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'; +import { Button } from './ui/button'; + +interface QRCodeModalProps { + roomId: string; +} + +export const QRCodeModal: React.FC = ({ roomId }: QRCodeModalProps) => { + const joinUrl = `https://www.beatsync.gg/room/${roomId}`; + return ( + + + + + +
+ +

Scan this on another device to join the session

+
+
+
+ ); +}; \ No newline at end of file diff --git a/apps/client/src/components/room/TopBar.tsx b/apps/client/src/components/room/TopBar.tsx index 32cfc279..eea23cab 100644 --- a/apps/client/src/components/room/TopBar.tsx +++ b/apps/client/src/components/room/TopBar.tsx @@ -1,24 +1,36 @@ +/// +/// +/// "use client"; import { useGlobalStore } from "@/store/global"; -import { Github, Hash, Users } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import Link from "next/link"; +import { Github, Hash, Users } from 'lucide-react'; +import { AnimatePresence, motion } from 'motion/react'; +import Link from 'next/link'; import { SyncProgress } from "../ui/SyncProgress"; +import { QRCodeModal } from '../QRCodeModal'; interface TopBarProps { roomId: string; } +declare global { + namespace JSX { + interface IntrinsicElements { + [elemName: string]: any; + } + } +} + export const TopBar = ({ roomId }: TopBarProps) => { - const isLoadingAudio = useGlobalStore((state) => state.isInitingSystem); - const isSynced = useGlobalStore((state) => state.isSynced); - const roundTripEstimate = useGlobalStore((state) => state.roundTripEstimate); - const sendNTPRequest = useGlobalStore((state) => state.sendNTPRequest); - const resetNTPConfig = useGlobalStore((state) => state.resetNTPConfig); - const pauseAudio = useGlobalStore((state) => state.pauseAudio); - const connectedClients = useGlobalStore((state) => state.connectedClients); - const setIsLoadingAudio = useGlobalStore((state) => state.setIsInitingSystem); - const clockOffset = useGlobalStore((state) => state.offsetEstimate); + const isLoadingAudio = useGlobalStore((state: any) => state.isInitingSystem); + const isSynced = useGlobalStore((state: any) => state.isSynced); + const roundTripEstimate = useGlobalStore((state: any) => state.roundTripEstimate); + const sendNTPRequest = useGlobalStore((state: any) => state.sendNTPRequest); + const resetNTPConfig = useGlobalStore((state: any) => state.resetNTPConfig); + const pauseAudio = useGlobalStore((state: any) => state.pauseAudio); + const connectedClients = useGlobalStore((state: any) => state.connectedClients); + const setIsLoadingAudio = useGlobalStore((state: any) => state.setIsInitingSystem); + const clockOffset = useGlobalStore((state: any) => state.offsetEstimate); const resync = () => { try { pauseAudio({ when: 0 }); @@ -76,6 +88,10 @@ export const TopBar = ({ roomId }: TopBarProps) => { > Full Sync + {/* Add QR Code Modal button */} +
+ +
{/* GitHub icon in the top right */} diff --git a/bun.lock b/bun.lock index fa8a41f3..9bbf9897 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "beatsync", "dependencies": { + "react-qr-code": "^2.0.15", "turbo": "^2.4.4", }, "devDependencies": { @@ -820,6 +821,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], @@ -832,6 +835,8 @@ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-qr-code": ["react-qr-code@2.0.15", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw=="], + "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], diff --git a/package.json b/package.json index 2a179187..a6df6a10 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "react-qr-code": "^2.0.15", "turbo": "^2.4.4" } } From ee464f792cf85916d1c20e6c173da9f8a01d9812 Mon Sep 17 00:00:00 2001 From: ShubhamSPawde Date: Thu, 8 May 2025 03:24:40 +0530 Subject: [PATCH 2/6] feat: add QR code for room join --- .../.cursor/rules/posthog-integration.mdc | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/client/.cursor/rules/posthog-integration.mdc diff --git a/apps/client/.cursor/rules/posthog-integration.mdc b/apps/client/.cursor/rules/posthog-integration.mdc new file mode 100644 index 00000000..f6cbda8f --- /dev/null +++ b/apps/client/.cursor/rules/posthog-integration.mdc @@ -0,0 +1,29 @@ +--- +description: apply when interacting with PostHog/analytics tasks +globs: +alwaysApply: true +--- + +Never hallucinate an API key. Instead, always use the API key populated in the .env file. + +# Feature flags + +A given feature flag should be used in as few places as possible. Do not increase the risk of undefined behavior by scattering the same feature flag across multiple areas of code. If the same feature flag needs to be introduced at multiple callsites, flag this for the developer to inspect carefully. + +If a job requires creating new feature flag names, make them as clear and descriptive as possible. + +If using TypeScript, use an enum to store flag names. If using JavaScript, store flag names as strings to an object declared as a constant, to simulate an enum. Use a consistent naming convention for this storage. enum/const object members should be written UPPERCASE_WITH_UNDERSCORE. + +Gate flag-dependent code on a check that verifies the flag's values are valid and expected. + +# Custom properties + +If a custom property for a person or event is at any point referenced in two or more files or two or more callsites in the same file, use an enum or const object, as above in feature flags. + +# Naming + +Before creating any new event or property names, consult with the developer for any existing naming convention. Consistency in naming is essential, and additional context may exist outside this project. Similarly, be careful about any changes to existing event and property names, as this may break reporting and distort data for the project. + + + + From 882b17ac3ab36c302b6abf26ec75b3e0b7f7f373 Mon Sep 17 00:00:00 2001 From: ShubhamSPawde Date: Thu, 8 May 2025 23:44:16 +0530 Subject: [PATCH 3/6] feat: finalize QR code join with polished UI --- apps/client/src/components/QRCodeModal.tsx | 5 +-- apps/client/src/components/room/TopBar.tsx | 46 +++++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/client/src/components/QRCodeModal.tsx b/apps/client/src/components/QRCodeModal.tsx index 190c30fa..5e23104f 100644 --- a/apps/client/src/components/QRCodeModal.tsx +++ b/apps/client/src/components/QRCodeModal.tsx @@ -1,18 +1,17 @@ import React from 'react'; import QRCode from 'react-qr-code'; import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'; -import { Button } from './ui/button'; interface QRCodeModalProps { roomId: string; } export const QRCodeModal: React.FC = ({ roomId }: QRCodeModalProps) => { - const joinUrl = `https://www.beatsync.gg/room/${roomId}`; + const joinUrl = `${window.location.origin}/room/${roomId}`; return ( - + QR Code
diff --git a/apps/client/src/components/room/TopBar.tsx b/apps/client/src/components/room/TopBar.tsx index eea23cab..53e37d79 100644 --- a/apps/client/src/components/room/TopBar.tsx +++ b/apps/client/src/components/room/TopBar.tsx @@ -13,24 +13,39 @@ interface TopBarProps { roomId: string; } -declare global { - namespace JSX { - interface IntrinsicElements { - [elemName: string]: any; - } - } +interface ConnectedClient { + username: string; + clientId: string; + rtt: number; + position: { + x: number; + y: number; + }; + ws?: any; +} + +interface GlobalState { + isInitingSystem: boolean; + isSynced: boolean; + roundTripEstimate: number; + sendNTPRequest: () => void; + resetNTPConfig: () => void; + pauseAudio: (params: { when: number }) => void; + connectedClients: ConnectedClient[]; + setIsInitingSystem: (value: boolean) => void; + offsetEstimate: number; } export const TopBar = ({ roomId }: TopBarProps) => { - const isLoadingAudio = useGlobalStore((state: any) => state.isInitingSystem); - const isSynced = useGlobalStore((state: any) => state.isSynced); - const roundTripEstimate = useGlobalStore((state: any) => state.roundTripEstimate); - const sendNTPRequest = useGlobalStore((state: any) => state.sendNTPRequest); - const resetNTPConfig = useGlobalStore((state: any) => state.resetNTPConfig); - const pauseAudio = useGlobalStore((state: any) => state.pauseAudio); - const connectedClients = useGlobalStore((state: any) => state.connectedClients); - const setIsLoadingAudio = useGlobalStore((state: any) => state.setIsInitingSystem); - const clockOffset = useGlobalStore((state: any) => state.offsetEstimate); + const isLoadingAudio = useGlobalStore((state) => state.isInitingSystem); + const isSynced = useGlobalStore((state) => state.isSynced); + const roundTripEstimate = useGlobalStore((state) => state.roundTripEstimate); + const sendNTPRequest = useGlobalStore((state) => state.sendNTPRequest); + const resetNTPConfig = useGlobalStore((state) => state.resetNTPConfig); + const pauseAudio = useGlobalStore((state) => state.pauseAudio); + const connectedClients = useGlobalStore((state) => state.connectedClients); + const setIsLoadingAudio = useGlobalStore((state) => state.setIsInitingSystem); + const clockOffset = useGlobalStore((state) => state.offsetEstimate); const resync = () => { try { pauseAudio({ when: 0 }); @@ -89,6 +104,7 @@ export const TopBar = ({ roomId }: TopBarProps) => { Full Sync {/* Add QR Code Modal button */} +
|
From f24d1a4e6c5bdd10a8807e66704836cfd680ad4e Mon Sep 17 00:00:00 2001 From: ShubhamSPawde Date: Fri, 9 May 2025 09:19:33 +0530 Subject: [PATCH 4/6] refactor : use global for audio control and client state --- apps/client/src/components/room/TopBar.tsx | 13 +------------ apps/client/src/store/global.tsx | 3 ++- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/client/src/components/room/TopBar.tsx b/apps/client/src/components/room/TopBar.tsx index 53e37d79..20dfb795 100644 --- a/apps/client/src/components/room/TopBar.tsx +++ b/apps/client/src/components/room/TopBar.tsx @@ -3,6 +3,7 @@ /// "use client"; import { useGlobalStore } from "@/store/global"; +import type { GlobalState } from "@/store/global"; import { Github, Hash, Users } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import Link from 'next/link'; @@ -24,18 +25,6 @@ interface ConnectedClient { ws?: any; } -interface GlobalState { - isInitingSystem: boolean; - isSynced: boolean; - roundTripEstimate: number; - sendNTPRequest: () => void; - resetNTPConfig: () => void; - pauseAudio: (params: { when: number }) => void; - connectedClients: ConnectedClient[]; - setIsInitingSystem: (value: boolean) => void; - offsetEstimate: number; -} - export const TopBar = ({ roomId }: TopBarProps) => { const isLoadingAudio = useGlobalStore((state) => state.isInitingSystem); const isSynced = useGlobalStore((state) => state.isSynced); diff --git a/apps/client/src/store/global.tsx b/apps/client/src/store/global.tsx index 9ff63c33..96ee0b86 100644 --- a/apps/client/src/store/global.tsx +++ b/apps/client/src/store/global.tsx @@ -80,7 +80,7 @@ interface GlobalStateValues { isShuffled: boolean; } -interface GlobalState extends GlobalStateValues { +export interface GlobalState extends GlobalStateValues { // Methods setIsInitingSystem: (isIniting: boolean) => void; addToUploadHistory: (name: string, id: string) => void; @@ -127,6 +127,7 @@ interface GlobalState extends GlobalStateValues { getCurrentGainValue: () => number; resetStore: () => void; } + // Audio sources const STATIC_AUDIO_SOURCES: StaticAudioSource[] = [ { From bb07c801b1ce970af480793b3a902130d43f7a75 Mon Sep 17 00:00:00 2001 From: ShubhamSPawde Date: Fri, 9 May 2025 09:33:58 +0530 Subject: [PATCH 5/6] refactor(TopBar): remove unused type imports and interfaces --- apps/client/src/components/room/TopBar.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apps/client/src/components/room/TopBar.tsx b/apps/client/src/components/room/TopBar.tsx index 20dfb795..bc10adb0 100644 --- a/apps/client/src/components/room/TopBar.tsx +++ b/apps/client/src/components/room/TopBar.tsx @@ -3,7 +3,6 @@ /// "use client"; import { useGlobalStore } from "@/store/global"; -import type { GlobalState } from "@/store/global"; import { Github, Hash, Users } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import Link from 'next/link'; @@ -14,17 +13,6 @@ interface TopBarProps { roomId: string; } -interface ConnectedClient { - username: string; - clientId: string; - rtt: number; - position: { - x: number; - y: number; - }; - ws?: any; -} - export const TopBar = ({ roomId }: TopBarProps) => { const isLoadingAudio = useGlobalStore((state) => state.isInitingSystem); const isSynced = useGlobalStore((state) => state.isSynced); From 4ff3b1243f4ef385e2d7bd0686f6366cd362621d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 07:40:24 +0000 Subject: [PATCH 6/6] feat: Implement room link sharing with Web Share API and clipboard fallback This commit enhances the QR code modal to allow you to more easily share room links. Key changes: - The text below the QR code is now "Click here to share link". - Clicking the QR code or the text attempts to use the Web Share API (`navigator.share`) to initiate native sharing. - If the Web Share API is unavailable or the share action is canceled/fails, it falls back to copying the room's join URL to the clipboard. - Feedback messages (e.g., "Sharing...", "Link shared!", "Link copied!") are displayed to inform you of the action's status. - Joining a room via the shared/copied link has been verified to ensure you are correctly synced. --- apps/client/src/components/QRCodeModal.tsx | 58 ++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/apps/client/src/components/QRCodeModal.tsx b/apps/client/src/components/QRCodeModal.tsx index 5e23104f..b000ac2d 100644 --- a/apps/client/src/components/QRCodeModal.tsx +++ b/apps/client/src/components/QRCodeModal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import QRCode from 'react-qr-code'; import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'; @@ -8,6 +8,53 @@ interface QRCodeModalProps { export const QRCodeModal: React.FC = ({ roomId }: QRCodeModalProps) => { const joinUrl = `${window.location.origin}/room/${roomId}`; + const [feedbackMessage, setFeedbackMessage] = useState(''); + + const clearFeedback = () => { + setTimeout(() => { + setFeedbackMessage(''); + }, 3000); + }; + + const handleShareOrCopy = async () => { + const shareData = { + title: 'Join my session', + text: `Join my session using this link: ${joinUrl}`, + url: joinUrl, + }; + + if (navigator.share) { + try { + setFeedbackMessage('Sharing...'); + await navigator.share(shareData); + setFeedbackMessage('Link shared!'); + clearFeedback(); + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + setFeedbackMessage('Share canceled'); + } else { + // Fallback to copy if navigator.share fails for other reasons + try { + await navigator.clipboard.writeText(joinUrl); + setFeedbackMessage('Link copied!'); + } catch (copyError) { + setFeedbackMessage('Failed to copy link'); + } + } + clearFeedback(); + } + } else { + // Fallback to copy if navigator.share is not available + try { + await navigator.clipboard.writeText(joinUrl); + setFeedbackMessage('Link copied!'); + } catch (copyError) { + setFeedbackMessage('Failed to copy link'); + } + clearFeedback(); + } + }; + return ( @@ -15,8 +62,13 @@ export const QRCodeModal: React.FC = ({ roomId }: QRCodeModalP
- -

Scan this on another device to join the session

+
+ +
+

+ Click here to share link +

+ {feedbackMessage &&

{feedbackMessage}

}