diff --git a/bsg-frontend/.gitignore b/bsg-frontend/.gitignore
index 228f3fa..7096335 100644
--- a/bsg-frontend/.gitignore
+++ b/bsg-frontend/.gitignore
@@ -5,3 +5,4 @@
/**/package-lock.json
**/node_modules/
**/next-env.d.ts
+out/
\ No newline at end of file
diff --git a/bsg-frontend/app/extension/defaultPopup/contentScript.js b/bsg-frontend/app/extension/defaultPopup/contentScript.js
new file mode 100644
index 0000000..07b7034
--- /dev/null
+++ b/bsg-frontend/app/extension/defaultPopup/contentScript.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import DefaultPopup from './page.tsx';
+
+const container = document.createElement('div');
+container.id = 'my-extension-root';
+document.body.appendChild(container);
+
+Object.assign(container.style, {
+ position: 'fixed',
+ top: '0',
+ right: '0',
+ width: '30%',
+ height: '100%',
+ backgroundColor: '#000',
+ zIndex: 9999,
+ overflow: 'auto'
+});
+
+const root = createRoot(container);
+root.render();
\ No newline at end of file
diff --git a/bsg-frontend/apps/extension/.env.example b/bsg-frontend/apps/extension/.env.example
index 1a54794..eb71c8f 100644
--- a/bsg-frontend/apps/extension/.env.example
+++ b/bsg-frontend/apps/extension/.env.example
@@ -2,10 +2,10 @@
# Copy this file to .env.local and fill in your Firebase project values
# Get these values from Firebase Console > Project Settings
-NEXT_PUBLIC_FIREBASE_API_KEY=
-NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
-NEXT_PUBLIC_FIREBASE_PROJECT_ID=
-NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
-NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
-NEXT_PUBLIC_FIREBASE_APP_ID=
-NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
+NEXT_PUBLIC_FIREBASE_API_KEY=""
+NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=""
+NEXT_PUBLIC_FIREBASE_PROJECT_ID=""
+NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=""
+NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=""
+NEXT_PUBLIC_FIREBASE_APP_ID=""
+NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=""
diff --git a/bsg-frontend/apps/extension/.gitignore b/bsg-frontend/apps/extension/.gitignore
index 693eb9d..f3f6c47 100644
--- a/bsg-frontend/apps/extension/.gitignore
+++ b/bsg-frontend/apps/extension/.gitignore
@@ -3,3 +3,4 @@
!.env.example
.env.local
.env
+out/
\ No newline at end of file
diff --git a/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/chromeExtensionAuth.ts b/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/chromeExtensionAuth.ts
index e9bae46..1924e37 100644
--- a/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/chromeExtensionAuth.ts
+++ b/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/chromeExtensionAuth.ts
@@ -1,7 +1,5 @@
-import { app } from "../../../config";
-import { getAuth, signInWithCredential, GoogleAuthProvider, User } from "firebase/auth";
-
-const auth = getAuth(app);
+import { getFirebaseAuth } from "../../../config";
+import { signInWithCredential, GoogleAuthProvider, User } from "firebase/auth";
export async function SignInWithChromeIdentity(): Promise {
return new Promise((resolve, reject) => {
@@ -22,8 +20,12 @@ export async function SignInWithChromeIdentity(): Promise {
}
try {
- // Use the token to get user info from Google API
- //const userInfo = await getUserInfoFromToken(token);
+ // get auth lazily (may be null during server/build)
+ const auth = getFirebaseAuth();
+ if (!auth) {
+ reject(new Error('Firebase auth not available in this environment'));
+ return;
+ }
// Create a Firebase credential using the token
const credential = GoogleAuthProvider.credential(null, token);
@@ -40,7 +42,7 @@ export async function SignInWithChromeIdentity(): Promise {
});
}
-async function getUserInfoFromToken(token: string) {
+export async function getUserInfoFromToken(token: string) {
const response = await fetch(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${token}`);
if (!response.ok) {
throw new Error('Failed to get user info');
@@ -59,9 +61,7 @@ export async function SignOutFromChrome(): Promise {
chrome.identity.getAuthToken({ interactive: false }, (token) => {
const tokenToRevoke = token.toString();
- const requestBody = new URLSearchParams({
- token: tokenToRevoke,
- }).toString();
+ const requestBody = new URLSearchParams({ token: tokenToRevoke }).toString();
fetch('https://oauth2.googleapis.com/revoke', {
method:'POST',
@@ -74,10 +74,17 @@ export async function SignOutFromChrome(): Promise {
if(response.ok){
console.log("Reached google servers")
- chrome.identity.clearAllCachedAuthTokens(() => {
- auth.signOut().then(resolve).catch(reject);
- }
- )
+ chrome.identity.clearAllCachedAuthTokens(async () => {
+ const auth = getFirebaseAuth();
+ if (auth) {
+ try {
+ await auth.signOut();
+ } catch (e) {
+ // ignore sign out error
+ }
+ }
+ resolve();
+ });
}
else {
diff --git a/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/googleAuth.ts b/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/googleAuth.ts
index 012945d..52b0425 100644
--- a/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/googleAuth.ts
+++ b/bsg-frontend/apps/extension/firebase/auth/signIn/googleImplementation/googleAuth.ts
@@ -1,20 +1,19 @@
-import { app } from "../../../config";
-import { getAuth,
- signInWithRedirect,
- signInWithPopup,
- getRedirectResult,
- User,
- } from "firebase/auth";
+import { getFirebaseAuth } from "../../../config";
+import {
+ signInWithRedirect,
+ signInWithPopup,
+ getRedirectResult,
+ User,
+} from "firebase/auth";
import { provider } from './googleSignIn';
-const auth = getAuth(app);
-
-
export async function SignInWithGoogleRedirect(): Promise {
try{
+ const auth = getFirebaseAuth();
+ if (!auth) throw new Error('Firebase auth not available in this environment');
await signInWithRedirect(auth, provider);
}
catch(error){
@@ -26,6 +25,8 @@ export async function SignInWithGoogleRedirect(): Promise {
export async function SignInWithGooglePopup(): Promise {
try {
+ const auth = getFirebaseAuth();
+ if (!auth) throw new Error('Firebase auth not available in this environment');
const result = await signInWithPopup(auth, provider);
return result.user;
} catch (error) {
@@ -38,6 +39,9 @@ export async function HandleAuthRedirectedResult(): Promise{
try{
+ const auth = getFirebaseAuth();
+ if (!auth) throw new Error('Firebase auth not available in this environment');
+
const result = await getRedirectResult(auth, provider);
if(result){
diff --git a/bsg-frontend/apps/extension/firebase/auth/signOut.ts b/bsg-frontend/apps/extension/firebase/auth/signOut.ts
index 65b8366..a8ae5d3 100644
--- a/bsg-frontend/apps/extension/firebase/auth/signOut.ts
+++ b/bsg-frontend/apps/extension/firebase/auth/signOut.ts
@@ -1,19 +1,18 @@
-import { signOut, getAuth } from "firebase/auth";
-import { app } from "../config";
+import { signOut } from "firebase/auth";
+import { getFirebaseAuth } from "../config";
+export async function signOutOfAccount(): Promise {
+ const auth = getFirebaseAuth();
+ if (!auth) {
+ // Nothing to do when auth isn't available (server/build environment)
+ return;
+ }
-export async function signOutOfAccount(): Promise{
-
- const auth = getAuth(app);
-
- try{
+ try {
return await signOut(auth);
-
- }catch(error){
-
- console.log(error.code)
- console.log(error.message)
+ } catch (error: any) {
+ console.log(error?.code);
+ console.log(error?.message);
throw error;
}
-
}
\ No newline at end of file
diff --git a/bsg-frontend/apps/extension/firebase/config.ts b/bsg-frontend/apps/extension/firebase/config.ts
index 7b329b5..23a89c4 100644
--- a/bsg-frontend/apps/extension/firebase/config.ts
+++ b/bsg-frontend/apps/extension/firebase/config.ts
@@ -1,8 +1,6 @@
// Import the functions you need from the SDKs you need
-import { initializeApp } from "firebase/app";
-import { getAuth, setPersistence, browserSessionPersistence } from "firebase/auth";
-
-
+import { initializeApp, FirebaseApp } from "firebase/app";
+import { getAuth, setPersistence, browserSessionPersistence, Auth } from "firebase/auth";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
@@ -14,11 +12,39 @@ const firebaseConfig = {
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
};
-// Initialize Firebase
-export const app = initializeApp(firebaseConfig);
+let firebaseApp: FirebaseApp | null = null;
+
+/**
+ * Lazily initialize and return the Firebase app instance.
+ * Returns null when run on the server or if the required env is missing.
+ */
+export function getFirebaseApp(): FirebaseApp | null {
+ if (typeof window === 'undefined') return null;
+ if (firebaseApp) return firebaseApp;
+
+ if (!firebaseConfig.apiKey) {
+ // Do not initialize Firebase in environments without an API key.
+ // This prevents build-time/server-side initialization which causes
+ // auth/invalid-api-key errors during next build when env files aren't loaded.
+ return null;
+ }
+
+ firebaseApp = initializeApp(firebaseConfig);
+ return firebaseApp;
+}
-// Initialize Auth with session persistence
-export const auth = getAuth(app);
-setPersistence(auth, browserSessionPersistence);
+/**
+ * Lazily return an Auth instance, or null if unavailable (server or missing config).
+ */
+export function getFirebaseAuth(): Auth | null {
+ const app = getFirebaseApp();
+ if (!app) return null;
+ const auth = getAuth(app);
+ // Only set browser persistence in a real browser environment
+ if (typeof window !== 'undefined') {
+ setPersistence(auth, browserSessionPersistence).catch(() => {});
+ }
+ return auth;
+}
diff --git a/bsg-frontend/apps/extension/hooks/useChatSocket.ts b/bsg-frontend/apps/extension/hooks/useChatSocket.ts
new file mode 100644
index 0000000..60f2322
--- /dev/null
+++ b/bsg-frontend/apps/extension/hooks/useChatSocket.ts
@@ -0,0 +1,106 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+
+const RTC_SERVICE_URL = 'ws://localhost:8080/ws';
+
+export type Message = {
+ userHandle: string;
+ data: string;
+ roomID: string;
+ isSystem?: boolean;
+}
+
+export const useChatSocket = (userEmail: string | null | undefined) => {
+ const socketRef = useRef(null);
+ const [messages, setMessages] = useState([]);
+ const [isConnected, setIsConnected] = useState(false);
+
+ useEffect(() => {
+ if (!userEmail) return;
+
+ const ws = new WebSocket(RTC_SERVICE_URL);
+ socketRef.current = ws;
+
+ ws.onopen = () => {
+ console.log('Connected to RTC service');
+ setIsConnected(true);
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const response = JSON.parse(event.data);
+
+ if (response.status === 'ok') {
+ const { message, responseType } = response;
+
+ if (responseType === 'chat-message') {
+ setMessages(prev => [...prev, {
+ userHandle: message.userHandle,
+ data: message.data,
+ roomID: message.roomID,
+ isSystem: false
+ }]);
+ } else if (responseType === 'system-announcement') {
+ setMessages(prev => [...prev, {
+ userHandle: 'System',
+ data: message.data,
+ roomID: message.roomID,
+ isSystem: true
+ }]);
+ }
+ } else if (response.status === 'error') {
+ console.error('RTC Error:', response.message);
+ }
+ } catch (e) {
+ console.error('Failed to parse WS message', e);
+ }
+ };
+
+ ws.onclose = () => {
+ console.log('Disconnected from RTC service');
+ setIsConnected(false);
+ };
+
+ return () => {
+ ws.close();
+ };
+ }, [userEmail]);
+
+ const joinRoom = useCallback((roomID: string) => {
+ // Clear messages when joining a new room so we don't see chat history from previous rooms
+ setMessages([]);
+
+ if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN && userEmail) {
+ const payload = {
+ name: userEmail,
+ "request-type": "join-room",
+ data: JSON.stringify({
+ userHandle: userEmail,
+ roomID: roomID
+ })
+ };
+ socketRef.current.send(JSON.stringify(payload));
+ }
+ }, [userEmail]);
+
+ const sendChatMessage = useCallback((roomID: string, message: string) => {
+ if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN && userEmail) {
+ const payload = {
+ name: userEmail,
+ "request-type": "chat-message",
+ data: JSON.stringify({
+ userHandle: userEmail,
+ roomID: roomID,
+ message: message
+ })
+ };
+ socketRef.current.send(JSON.stringify(payload));
+ }
+ }, [userEmail]);
+
+ return {
+ messages,
+ isConnected,
+ joinRoom,
+ sendChatMessage
+ };
+};
\ No newline at end of file
diff --git a/bsg-frontend/apps/extension/out/404.html b/bsg-frontend/apps/extension/out/404.html
index a2a205a..9bdddf2 100644
--- a/bsg-frontend/apps/extension/out/404.html
+++ b/bsg-frontend/apps/extension/out/404.html
@@ -1 +1 @@
-404: This page could not be found
404
This page could not be found.
\ No newline at end of file
+
BSG_
\ No newline at end of file
diff --git a/bsg-frontend/apps/extension/out/defaultPopup.html b/bsg-frontend/apps/extension/out/defaultPopup.html
index cdf98f4..e61c0f9 100644
--- a/bsg-frontend/apps/extension/out/defaultPopup.html
+++ b/bsg-frontend/apps/extension/out/defaultPopup.html
@@ -1 +1 @@
-
BSG_
You are not on LeetCode. Once you go to the website you can open up the side panel to start solving!
\ No newline at end of file
+
BSG_
\ No newline at end of file
diff --git a/bsg-frontend/apps/extension/out/manifest.json b/bsg-frontend/apps/extension/out/manifest.json
index 878a68e..0bbf310 100644
--- a/bsg-frontend/apps/extension/out/manifest.json
+++ b/bsg-frontend/apps/extension/out/manifest.json
@@ -2,6 +2,9 @@
"manifest_version": 3,
"name": "BSG",
"version": "1.0.0",
+ "background": {
+ "service_worker": "background.js"
+ },
"permissions": [
"tabs",
"identity"
@@ -25,7 +28,7 @@
"default_popup": "defaultPopup.html"
},
"content_security_policy": {
- "extension_pages": "script-src 'self'; script-src-elem 'self' https://apis.google.com https://accounts.google.com https://www.gstatic.com; connect-src 'self' https://*.googleapis.com https://*.firebaseapp.com https://accounts.google.com; object-src 'self';"
+ "extension_pages": "script-src 'self'; script-src-elem 'self' https://apis.google.com https://accounts.google.com https://www.gstatic.com; connect-src 'self' ws://localhost:8080 https://*.googleapis.com https://*.firebaseapp.com https://accounts.google.com; object-src 'self';"
},
"web_accessible_resources": [
{
diff --git a/bsg-frontend/apps/extension/pages/_app.tsx b/bsg-frontend/apps/extension/pages/_app.tsx
index 4db9a90..74fa05e 100644
--- a/bsg-frontend/apps/extension/pages/_app.tsx
+++ b/bsg-frontend/apps/extension/pages/_app.tsx
@@ -1,22 +1,262 @@
import '../../../packages/ui-styles/global.css'
-import Logo from '@bsg/components/Logo'
import { useState, useRef, useEffect } from 'react'
import type { AppProps } from 'next/app'
import '@bsg/ui-styles/global.css';
import {Poppins} from 'next/font/google'
import { Button } from '@bsg/ui/button'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faDiscord, faGithub, faGoogle } from '@fortawesome/free-brands-svg-icons'
-import { faPaperPlane, faSmile } from '@fortawesome/free-solid-svg-icons'
+import { faPaperPlane, faSmile, faCopy } from '@fortawesome/free-solid-svg-icons'
+import { faGoogle } from '@fortawesome/free-brands-svg-icons'
+import RoomChoice from './room-choice'
+import { SignInWithChromeIdentity, getUserInfoFromToken } from '../firebase/auth/signIn/googleImplementation/chromeExtensionAuth'
+import { useChatSocket } from '../hooks/useChatSocket'
-const poppins = Poppins({weight: '400', subsets: ['latin'], variable: '--poppins'})
+const poppins = Poppins({ weight: '400', subsets: ['latin'] })
+
+type Participant = { id: string; name?: string; avatarUrl?: string }
export default function App({ Component, pageProps }: AppProps) {
+ const [loggedIn, setLoggedIn] = useState(false)
+ const [currentRoom, setCurrentRoom] = useState<{ code: string, options?: any } | null>(null)
+ const [copied, setCopied] = useState(false)
+ const [userProfile, setUserProfile] = useState(null)
+
+ const inputRef = useRef(null)
+ const containerRef = useRef(null)
+
+ // Initialize WebSocket Hook
+ const { messages, isConnected, joinRoom, sendChatMessage } = useChatSocket(userProfile?.id);
+
+ // copy room code to clipboard (works in extension and locally)
+ function copyRoomCode(roomCode: string) {
+ if (!roomCode) return
+ try {
+ if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
+ chrome.runtime.sendMessage({ type: 'COPY_TO_CLIPBOARD', text: roomCode }, (resp) => {
+ const ok = resp && resp.ok
+ if (ok) {
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ return
+ }
+ doLocalCopy(roomCode)
+ })
+ return
+ }
+ } catch (e) {
+ // fallback
+ }
+ doLocalCopy(roomCode)
+ }
+
+ function doLocalCopy(roomCode: string) {
+ const ta = document.createElement('textarea')
+ ta.value = roomCode
+ ta.style.position = 'fixed'
+ ta.style.left = '-9999px'
+ document.body.appendChild(ta)
+ ta.select()
+ try { document.execCommand('copy') } catch {}
+ ta.remove()
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ function sendMessage() {
+ const text = inputRef.current?.value.trim()
+ if (!text || !currentRoom) return
+
+ // Send message via WebSocket
+ sendChatMessage(currentRoom.code, text);
+
+ // REMOVED: Optimistic update.
+ // The server will echo the message back to us, so we don't need to add it manually here.
+ // This prevents the "double message" issue for the sender.
+
+ inputRef.current!.value = ''
+ }
+
+ useEffect(() => {
+ if (containerRef.current) {
+ containerRef.current.scrollTop = containerRef.current.scrollHeight
+ }
+ }, [messages])
+
+ // join/create handlers
+ const handleJoin = (roomCode: string) => {
+ setCurrentRoom({ code: roomCode, options: {} })
+ joinRoom(roomCode);
+ }
+
+ const handleCreate = (roomCode: string, options: any) => {
+ setCurrentRoom({ code: roomCode, options: { ...options } })
+ // Currently just joins the room code generated.
+ // Future: Send 'create-room' request if backend distinguishes it.
+ joinRoom(roomCode);
+ }
+
+ // --- RENDER LOGIC ---
+ if (!loggedIn) {
+ return (
+
+
+
+ BSG_
+
+
+
+
+
+
+ )
+ }
+
+ // Show RoomChoice if not yet in a room
+ if (!currentRoom) {
+ return (
+
+ )
+ }
+
+ const participants: Participant[] = currentRoom.options?.participants || []
return (
-
-
+
+
+
+
+
Room Code:
+
+
{currentRoom.code}
+
+ {copied &&
copied
}
+ {!isConnected &&
Disconnected
}
+
+
+
+ {/* Participant avatars (lobby) */}
+
+ {participants.map((p) => (
+
+ ))}
+
+
+
+
+ {/* show current user's avatar */}
+ {userProfile && (
+
+ )}
+