style again

This commit is contained in:
Arkaprabha Chakraborty
2025-10-30 21:32:09 +05:30
parent 1b1b925cd5
commit f51fac6afd
4 changed files with 240 additions and 74 deletions

View File

@@ -1,38 +1,17 @@
import { NextRequest } from "next/server"; export async function GET() {
import { WebSocketServer } from "ws"; // In App Router, WebSocket connections should be handled differently
// This endpoint is mainly for checking WebSocket server availability
// The actual WebSocket server is running separately on the Go backend
interface ExtendedNextRequest extends NextRequest { const wsUrl = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8100/o/socket";
socket: {
server: { return new Response(JSON.stringify({
wss?: WebSocketServer; message: "WebSocket endpoint available",
}; wsUrl: wsUrl,
on: (event: string, listener: (...args: any[]) => void) => void; timestamp: new Date().toISOString()
}; }), {
} headers: {
"Content-Type": "application/json",
export async function GET(req: ExtendedNextRequest) { },
const { socket } = req; });
if (!socket?.server?.wss) {
const wss = new WebSocketServer({ noServer: true });
socket.server.wss = wss;
wss.on("connection", (ws) => {
ws.on("message", (message) => {
wss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(message.toString());
}
});
});
});
socket.on("upgrade", (request: any, socket: any, head: any) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
});
}
return new Response("WebSocket setup complete");
} }

View File

@@ -219,7 +219,73 @@ const Room = () => {
const [windowWidth, setWindowWidth] = useState(0); const [windowWidth, setWindowWidth] = useState(0);
const [leftPanelForced, setLeftPanelForced] = useState(false); const [leftPanelForced, setLeftPanelForced] = useState(false);
const [rightPanelForced, setRightPanelForced] = useState(false); const [rightPanelForced, setRightPanelForced] = useState(false);
const [popupMessage, setPopupMessage] = useState<string | null>(null); const [popupMessage, setPopupMessage] = useState<{text: string; type?: 'default' | 'warning'} | null>(null);
const [isMobile, setIsMobile] = useState(false);
// Detect mobile screen size
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768); // md breakpoint
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Mobile swipe gesture handling
useEffect(() => {
if (!isMobile) return;
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;
const handleTouchStart = (e: TouchEvent) => {
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
touchStartTime = Date.now();
};
const handleTouchEnd = (e: TouchEvent) => {
const touch = e.changedTouches[0];
const touchEndX = touch.clientX;
const touchEndY = touch.clientY;
const touchEndTime = Date.now();
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
const deltaTime = touchEndTime - touchStartTime;
// Only consider it a swipe if:
// 1. The gesture is fast enough (less than 500ms)
// 2. The horizontal distance is significant (at least 100px)
// 3. The vertical distance is less than horizontal (to avoid conflicting with scrolling)
if (
deltaTime < 500 &&
Math.abs(deltaX) > 100 &&
Math.abs(deltaX) > Math.abs(deltaY)
) {
if (deltaX < 0 && leftPanelForced) {
// Swipe left - close left panel
setLeftPanelForced(false);
} else if (deltaX > 0 && rightPanelForced) {
// Swipe right - close right panel
setRightPanelForced(false);
}
}
};
document.addEventListener('touchstart', handleTouchStart, { passive: true });
document.addEventListener('touchend', handleTouchEnd, { passive: true });
return () => {
document.removeEventListener('touchstart', handleTouchStart);
document.removeEventListener('touchend', handleTouchEnd);
};
}, [isMobile, leftPanelForced, rightPanelForced]);
const contentRef = useRef(content); const contentRef = useRef(content);
@@ -593,11 +659,19 @@ const Room = () => {
const handleFileUpload = async (files: FileList) => { const handleFileUpload = async (files: FileList) => {
if (!files || files.length === 0 || !currentUser) return; if (!files || files.length === 0 || !currentUser) return;
const maxFileSize = 10 * 1024 * 1024; // 10MB in bytes
const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8081"; const httpUrl = process.env.NEXT_PUBLIC_HTTP_URL || "http://localhost:8081";
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
// Check file size limit
if (file.size > maxFileSize) {
const fileSizeInMB = (file.size / (1024 * 1024)).toFixed(2);
showPopup(`File "${file.name}" (${fileSizeInMB}MB) exceeds 10MB limit`, 'warning');
continue; // Skip this file and continue with others
}
try { try {
// Create form data for file upload // Create form data for file upload
const formData = new FormData(); const formData = new FormData();
@@ -651,8 +725,8 @@ const Room = () => {
} }
}; };
const showPopup = (message: string) => { const showPopup = (message: string, type: 'default' | 'warning' = 'default') => {
setPopupMessage(message); setPopupMessage({text: message, type});
setTimeout(() => setPopupMessage(null), 2000); setTimeout(() => setPopupMessage(null), 2000);
}; };
@@ -668,8 +742,8 @@ const Room = () => {
<div <div
className="absolute inset-0 transition-all duration-300" className="absolute inset-0 transition-all duration-300"
style={{ style={{
left: showLeftPanel ? '320px' : '0px', left: isMobile ? '0px' : (showLeftPanel ? '320px' : '0px'),
right: showRightPanel ? '320px' : '0px', right: isMobile ? '0px' : (showRightPanel ? '320px' : '0px'),
}} }}
> >
<div <div
@@ -762,6 +836,38 @@ const Room = () => {
{getThemeById(currentThemeId)?.name || "Switch theme"} {getThemeById(currentThemeId)?.name || "Switch theme"}
</HoverCardContent> </HoverCardContent>
</HoverCard> </HoverCard>
{/* Mobile Panel Controls */}
{isMobile && (
<>
<HoverCard>
<HoverCardTrigger>
<Button
className="bg-chart-1 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-1/80 btn-micro"
onClick={() => setLeftPanelForced(!leftPanelForced)}
>
media
</Button>
</HoverCardTrigger>
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
toggle users & media panel
</HoverCardContent>
</HoverCard>
<HoverCard>
<HoverCardTrigger>
<Button
className="bg-chart-3 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-3/80 btn-micro"
onClick={() => setRightPanelForced(!rightPanelForced)}
>
notes
</Button>
</HoverCardTrigger>
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
toggle comments panel
</HoverCardContent>
</HoverCard>
</>
)}
</div> </div>
</div> </div>
<div className="flex-grow flex flex-col p-1 w-full"> <div className="flex-grow flex flex-col p-1 w-full">
@@ -770,15 +876,25 @@ const Room = () => {
{error} {error}
</div> </div>
)} )}
<CodeEditor {isMobile ? (
ref={editorRef} <textarea
value={content} value={content}
onChange={handleContentChange} onChange={(e) => handleContentChange(e.target.value)}
onSelectionChange={handleSelectionChange} className="flex-grow w-full p-3 bg-background text-foreground border border-border rounded resize-none font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
language="plaintext" placeholder="Start typing your code here..."
className="flex-grow w-full" style={{ fontFamily: 'JetBrains Mono, Consolas, Monaco, "Courier New", monospace' }}
themeConfig={getThemeById(currentThemeId)} />
/> ) : (
<CodeEditor
ref={editorRef}
value={content}
onChange={handleContentChange}
onSelectionChange={handleSelectionChange}
language="plaintext"
className="flex-grow w-full"
themeConfig={getThemeById(currentThemeId)}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -801,7 +917,7 @@ const Room = () => {
{/* Comments Panel */} {/* Comments Panel */}
<CommentsPanel <CommentsPanel
isVisible={showRightPanel} isVisible={isMobile ? rightPanelForced : showRightPanel}
onToggle={() => setRightPanelForced(!rightPanelForced)} onToggle={() => setRightPanelForced(!rightPanelForced)}
selectedLineStart={selectedLineStart} selectedLineStart={selectedLineStart}
selectedLineEnd={selectedLineEnd} selectedLineEnd={selectedLineEnd}
@@ -814,8 +930,12 @@ const Room = () => {
{/* Custom Popup */} {/* Custom Popup */}
{popupMessage && ( {popupMessage && (
<div className="fixed top-4 right-4 z-50"> <div className="fixed top-4 right-4 z-50">
<div className="px-3 py-2 bg-popover text-popover-foreground border border-border rounded-lg shadow-lg animate-in fade-in slide-in-from-top-2 duration-200"> <div className={`px-3 py-2 border rounded-lg shadow-lg animate-in fade-in slide-in-from-top-2 duration-200 ${
<span className="text-sm font-medium">{popupMessage}</span> popupMessage.type === 'warning'
? 'bg-yellow-100 text-yellow-800 border-yellow-300 dark:bg-yellow-900 dark:text-yellow-300 dark:border-yellow-700'
: 'bg-popover text-popover-foreground border-border'
}`}>
<span className="text-sm font-medium">{popupMessage.text}</span>
</div> </div>
</div> </div>
)} )}
@@ -833,7 +953,7 @@ const Room = () => {
{/* Left Panel (Users, Media & ECG) */} {/* Left Panel (Users, Media & ECG) */}
<LeftPanel <LeftPanel
isVisible={showLeftPanel} isVisible={isMobile ? leftPanelForced : showLeftPanel}
users={users} users={users}
mediaFiles={mediaFiles} mediaFiles={mediaFiles}
onFileUpload={handleFileUpload} onFileUpload={handleFileUpload}

View File

@@ -113,6 +113,7 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
const [playingAudio, setPlayingAudio] = useState<string | null>(null); const [playingAudio, setPlayingAudio] = useState<string | null>(null);
const [modalFile, setModalFile] = useState<MediaFile | null>(null); const [modalFile, setModalFile] = useState<MediaFile | null>(null);
const [audioProgress, setAudioProgress] = useState<{ [key: string]: { currentTime: number; duration: number } }>({});
const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({}); const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({});
// Helper function to get the correct file URL using HTTP server // Helper function to get the correct file URL using HTTP server
@@ -202,9 +203,30 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
setPlayingAudio(null); setPlayingAudio(null);
} else { } else {
if (!audioRefs.current[fileId]) { if (!audioRefs.current[fileId]) {
audioRefs.current[fileId] = new Audio(url); const audio = new Audio(url);
audioRefs.current[fileId].addEventListener('ended', () => { audioRefs.current[fileId] = audio;
// Add event listeners for progress tracking
audio.addEventListener('loadedmetadata', () => {
setAudioProgress(prev => ({
...prev,
[fileId]: { currentTime: 0, duration: audio.duration }
}));
});
audio.addEventListener('timeupdate', () => {
setAudioProgress(prev => ({
...prev,
[fileId]: { currentTime: audio.currentTime, duration: audio.duration }
}));
});
audio.addEventListener('ended', () => {
setPlayingAudio(null); setPlayingAudio(null);
setAudioProgress(prev => ({
...prev,
[fileId]: { currentTime: 0, duration: audio.duration }
}));
}); });
} }
audioRefs.current[fileId].play(); audioRefs.current[fileId].play();
@@ -212,6 +234,20 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
} }
}; };
const handleSeekAudio = (fileId: string, seekTime: number) => {
const audio = audioRefs.current[fileId];
if (audio) {
audio.currentTime = seekTime;
}
};
const formatAudioTime = (seconds: number) => {
if (isNaN(seconds)) return '0:00';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
const handleDeleteFile = (fileId: string) => { const handleDeleteFile = (fileId: string) => {
// Call parent delete handler if available // Call parent delete handler if available
if (onFileDelete) { if (onFileDelete) {
@@ -240,7 +276,7 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
return ( return (
<div <div
className={`fixed left-0 top-0 h-full w-80 bg-card border-r border-border shadow-lg z-40 flex flex-col transition-transform duration-300 ease-in-out ui-font ${ className={`fixed left-0 top-0 h-full w-full md:w-80 bg-card border-r border-border shadow-lg z-40 flex flex-col transition-transform duration-300 ease-in-out ui-font ${
isVisible ? 'transform-none' : '-translate-x-full' isVisible ? 'transform-none' : '-translate-x-full'
}`} }`}
> >
@@ -423,21 +459,52 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
)} )}
{file.type.startsWith('audio/') && ( {file.type.startsWith('audio/') && (
<div className="mt-2 flex items-center space-x-2"> <div className="mt-2 space-y-2">
<Button <div className="flex items-center space-x-2">
size="sm" <Button
variant="outline" size="sm"
className="h-8 w-8 p-0" variant="outline"
onClick={() => handlePlayAudio(file.id, getFileUrl(file))} className="h-8 w-8 p-0"
> onClick={() => handlePlayAudio(file.id, getFileUrl(file))}
{playingAudio === file.id ? ( >
<Pause size={12} /> {playingAudio === file.id ? (
) : ( <Pause size={12} />
<Play size={12} /> ) : (
)} <Play size={12} />
</Button> )}
<div className="flex-1 h-2 bg-muted rounded-full"> </Button>
<div className="h-2 bg-primary rounded-full w-0"></div> <div className="flex-1 flex flex-col space-y-1">
<div
className="h-2 bg-muted rounded-full cursor-pointer hover:bg-muted/80 transition-colors relative"
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const width = rect.width;
const progress = audioProgress[file.id];
if (progress) {
const seekTime = (clickX / width) * progress.duration;
handleSeekAudio(file.id, seekTime);
}
}}
>
<div
className="h-2 bg-green-500 dark:bg-green-600 rounded-full transition-all duration-100"
style={{
width: audioProgress[file.id]
? `${(audioProgress[file.id].currentTime / audioProgress[file.id].duration) * 100}%`
: '0%'
}}
/>
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>
{audioProgress[file.id] ? formatAudioTime(audioProgress[file.id].currentTime) : '0:00'}
</span>
<span>
{audioProgress[file.id] ? formatAudioTime(audioProgress[file.id].duration) : '0:00'}
</span>
</div>
</div>
</div> </div>
</div> </div>
)} )}

View File

@@ -109,7 +109,7 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
return ( return (
<div <div
className={`fixed right-0 top-0 h-full w-80 bg-card border-l border-border shadow-lg z-40 flex flex-col transition-transform duration-300 ease-in-out ${ className={`fixed right-0 top-0 h-full w-full md:w-80 bg-card border-l border-border shadow-lg z-40 flex flex-col transition-transform duration-300 ease-in-out ${
isVisible ? 'transform-none' : 'translate-x-full' isVisible ? 'transform-none' : 'translate-x-full'
}`} }`}
> >