Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
# Secrets / local env
.env
serviceKey.json
serviceKey.json

# macOS
.DS_Store
**/.DS_Store

# Dependencies & build output (kept out of iCloud via *.nosync too)
node_modules/
**/node_modules/
.next/
**/.next/
out/
**/out/
*.nosync
1 change: 1 addition & 0 deletions bsg-frontend/.next 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
type NotificationToggleProps = {
enabled: boolean;
onChange: (enabled: boolean) => void;
disabled?: boolean;
};

export const NotificationToggle = ({
enabled,
onChange,
disabled = false,
}: NotificationToggleProps) => (
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label="Chat notifications"
disabled={disabled}
onClick={() => onChange(!enabled)}
className={`
relative h-9 w-[4.625rem] shrink-0 rounded-full
transition-colors duration-200 ease-in-out
disabled:cursor-not-allowed disabled:opacity-50
${enabled ? 'bg-primary' : 'border border-white/80 bg-transparent'}
`}
>
{/* Label sits in the empty half, opposite the knob */}
<span
className={`
pointer-events-none absolute inset-0 z-0 flex items-center
text-[11px] font-bold tracking-wide
${enabled ? 'justify-start pl-2.5 text-[#262626]' : 'justify-end pr-2.5 text-white'}
`}
>
{enabled ? 'ON' : 'OFF'}
</span>

{/* Knob — use left positioning instead of translate-x */}
<span
className={`
absolute top-1/2 z-10 h-7 w-7 -translate-y-1/2 rounded-full bg-white shadow-sm
transition-[left] duration-200 ease-in-out
${enabled ? 'left-[calc(100%-1.75rem-0.25rem)]' : 'left-1'}
`}
/>
</button>
);
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ChatDisplay = ({ isActive }: { isActive: boolean }) => {
} = useChatSocket();

const username = useUserStore(s => s.username);
const email = useUserStore(s => s.email); // ← add this line
const emojiMap = require('@bsg/ui-styles/assets/emojis.json') as Record<string, { emoji: string; name: string; keywords: string[] }[]>;
const emojiList = Object.entries(emojiMap).flatMap(([category, emojis]) =>
emojis.map(emoji => ({ ...emoji, category }))
Expand Down Expand Up @@ -76,7 +77,7 @@ export const ChatDisplay = ({ isActive }: { isActive: boolean }) => {
:

<>
{(group[0].userName === username) ?
{(group[0].userHandle === email) ? // just changed

// User's own message
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useEffect, useState } from 'react';
import { useRoomStore } from '@/stores/useRoomStore';
import { SERVER_URL } from '../../lib/config';
import { Label } from '@bsg/ui/label';
import { Slider } from '@bsg/ui/slider';
import { NotificationToggle } from '../NotificationToggle';
import { useSettingsStore } from '@/stores/useSettingsStore';

const MIN_DURATION = 5;
const MAX_DURATION = 120;
const STEP = 5;


export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => {

const roomId = useRoomStore(s => s.roomId);
const isAdmin = useRoomStore(s => s.isAdmin);
const isRoundStarted = useRoomStore(s => s.isRoundStarted);
const roundDuration = useRoomStore(s => s.roundDuration);
const setRoundDuration = useRoomStore(s => s.setRoundDuration);

const [ value, setValue ] = useState<number>(roundDuration ?? 30);
const [ saving, setSaving ] = useState<boolean>(false);
const [ error, setError ] = useState<string | null>(null);
const [ saved, setSaved ] = useState<boolean>(false);


const chatNotificationsEnabled = useSettingsStore(s => s.chatNotificationsEnabled);
const setChatNotificationsEnabled = useSettingsStore(s => s.setChatNotificationsEnabled);
const loadSettings = useSettingsStore(s => s.loadSettings);

useEffect(() => {
if (isActive) loadSettings();
}, [isActive, loadSettings]);


// Sync the working value when the stored duration changes or the tab reopens.
useEffect(() => {
if (roundDuration != null) setValue(roundDuration);
}, [roundDuration, isActive]);

const clamp = (n: number) => Math.max(MIN_DURATION, Math.min(MAX_DURATION, n));
const change = (next: number) => {
setValue(clamp(next));
setSaved(false);
setError(null);
};

const dirty = value !== roundDuration;

const onSave = async () => {
if (!roomId) {
setError('Not in a room.');
return;
}
setSaving(true);
setError(null);
try {
const res = await fetch(`${SERVER_URL}/rooms/${roomId}/rounds/time`, {
method: 'POST',
body: JSON.stringify({ duration: value }),
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const data = await res.json().catch(() => null);
if (!res.ok) {
setError((data && (data.error || data.message)) || `Failed to update timer: ${res.status}`);
} else {
setRoundDuration(data?.data?.duration ?? value);
setSaved(true);
}
} catch (e: any) {
setError(e?.message || 'Failed to update timer.');
} finally {
setSaving(false);
}
};

//timer update success message timeout
useEffect(() => {
if (!saved) return;

const timeout = setTimeout(() => {
setSaved(false);
}, 3000);

return () => clearTimeout(timeout);
}, [saved]);

return (
<div className={`flex flex-col h-full p-4 gap-4 ${(isActive) ? '' : 'hidden'}`}>
<div className="text-base font-medium">Settings</div>

<div className="flex flex-col gap-2 w-full">
<div className="text-sm text-foreground/60 font-medium">Edit Timer</div>

<div className="rounded-lg border border-[#454545] p-4">
{!isAdmin ? (
<p className="text-sm text-foreground/50">
Only the room admin can change the timer.
</p>
) : isRoundStarted ? (
<p className="text-sm text-foreground/50">
The timer can only be edited before the round starts.
</p>
) : (
<div>
<Label className="text-lg">Timer: {value} mins</Label>
<Slider
min={MIN_DURATION}
max={MAX_DURATION}
step={STEP}
value={[value]}
onValueChange={(val) => change(val[0])}
className="pt-2 max-w-[377px]"
/>
</div>
)}
</div>
</div>


<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">Chat sounds</span>
<span className="text-xs text-foreground/50">
Play a sound when messages are sent or received
</span>
</div>
<NotificationToggle
enabled={chatNotificationsEnabled}
onChange={setChatNotificationsEnabled}
/>
</div>

{isAdmin && !isRoundStarted && (
<div className="mt-auto">
{error && <p className="mb-2 text-xs text-red-400">{error}</p>}
{saved && !dirty && (
<p className="mb-2 text-xs text-green-400">Timer updated.</p>
)}
<button
type="button"
onClick={onSave}
disabled={saving || !dirty}
className="w-full rounded-md bg-white/10 py-2 text-sm font-medium hover:bg-[#484848] disabled:opacity-30"
>
{saving ? 'Saving…' : 'Save'}
</button>
</div>
)}
</div>
);
};
50 changes: 45 additions & 5 deletions bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
const activeTab = useRoomStore(s => s.activeTab);
const setActiveTab = useRoomStore(s => s.setActiveTab);
const isPanelHovered = usePanelStore(s => s.isPanelHovered);

const unreadCount = useRoomStore(s => s.unreadCount);

//to select tab and unfold the panel - sets active tab and unfolds the extension!
const selectTabAndUnfold = (tab: TabName) => {
setActiveTab(tab);
messageScript('UNFOLD');
};
return (
<div
onDoubleClick={() => messageScript('UNFOLD')}
Expand All @@ -32,7 +39,7 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
{/* Logo */}
<div className="flex flex-col w-full items-center bg-[#262626] pointer-events-auto">
<div className="flex pt-2 px-1 gap-1 font-medium text-sm">
<div className="w-5 h-5 flex items-center justify-center">
<div className="relative w-5 h-5 flex items-center justify-center">
<svg
viewBox="0 0 81 65"
fill="none"
Expand All @@ -46,6 +53,39 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
stroke-linejoin="round"
/>
</svg>

{unreadCount > 0 && (
<span
style={{
position: 'absolute',
top: -2,
right: -3,
width: 11,
height: 11,
borderRadius: '50%',
backgroundColor: '#ef4444',
color: '#ffffff',
fontSize: 7,
fontWeight: 700,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 50,
lineHeight: 1,
boxSizing: 'border-box',
border: '1px solid #262626',
}}
>
{unreadCount > 9 ? (
<>
<span style={{ fontSize: 7 }}>9</span>
<span style={{ fontSize: 5, marginTop: -1 }}>+</span>
</>
) : (
unreadCount
)}
</span>
)}
</div>
</div>
</div>
Expand All @@ -65,7 +105,7 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
<Button
onMouseEnter={() => setHoveredTab('roomInfo')}
onMouseLeave={() => setHoveredTab(null)}
onClick={() => setActiveTab('roomInfo')}
onClick={() => selectTabAndUnfold('roomInfo')}
className="flex h-auto py-2 px-1 bg-transparent hover:bg-[#434343] rounded-[5px]"
>
<div className={`flex flex-col gap-1 text-sm ${(activeTab === 'roomInfo') ? '' : 'opacity-60'}`}>
Expand Down Expand Up @@ -93,7 +133,7 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
<Button
onMouseEnter={() => setHoveredTab('chat')}
onMouseLeave={() => setHoveredTab(null)}
onClick={() => setActiveTab('chat')}
onClick={() => selectTabAndUnfold('chat')}
className="flex h-auto py-2 px-1 bg-transparent hover:bg-[#434343] rounded-[5px]"
>
<div className={`flex flex-col gap-1 text-sm ${(activeTab === 'chat') ? '' : 'opacity-60'}`}>
Expand All @@ -117,7 +157,7 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
<Button
onMouseEnter={() => setHoveredTab('leaderboard')}
onMouseLeave={() => setHoveredTab(null)}
onClick={() => setActiveTab('leaderboard')}
onClick={() => selectTabAndUnfold('leaderboard')}
className="flex h-auto py-2 px-1 bg-transparent hover:bg-[#434343] rounded-[5px]"
>
<div className={`flex flex-col gap-1 text-sm ${(activeTab === 'leaderboard') ? '' : 'opacity-60'}`}>
Expand Down Expand Up @@ -146,7 +186,7 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {
<Button
onMouseEnter={() => setHoveredTab('statistics')}
onMouseLeave={() => setHoveredTab(null)}
onClick={() => setActiveTab('statistics')}
onClick={() => selectTabAndUnfold('statistics')}
className="flex h-auto py-2 px-1 bg-transparent hover:bg-[#434343] rounded-[5px]"
>
<div className={`flex flex-col gap-1 text-sm ${(activeTab === 'statistics') ? '' : 'opacity-60'}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const Toolbar = () => {

const isAdmin = useRoomStore(s => s.isAdmin);
const isRoundStarted = useRoomStore(s => s.isRoundStarted);
const setActiveTab = useRoomStore(s => s.setActiveTab);
const { handleStartRound, handleEndRound, handleLeaveRoom } = useRoomEvents();

// TODO: Move timer into separate iframe
Expand Down Expand Up @@ -114,7 +115,7 @@ export const Toolbar = () => {

<TooltipWrapper text="Settings">
<Button
//onClick={}
onClick={() => setActiveTab('settings')}
className="rounded-[5px] p-0 h-6 w-6 flex items-center justify-center text-foreground/60 bg-transparent hover:bg-[#484848]"
>
<svg
Expand Down
Loading
Loading