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";
import { WebSocketServer } from "ws";
interface ExtendedNextRequest extends NextRequest {
socket: {
server: {
wss?: WebSocketServer;
};
on: (event: string, listener: (...args: any[]) => void) => void;
};
}
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");
export async function GET() {
// 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
const wsUrl = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8100/o/socket";
return new Response(JSON.stringify({
message: "WebSocket endpoint available",
wsUrl: wsUrl,
timestamp: new Date().toISOString()
}), {
headers: {
"Content-Type": "application/json",
},
});
}

View File

@@ -219,7 +219,73 @@ const Room = () => {
const [windowWidth, setWindowWidth] = useState(0);
const [leftPanelForced, setLeftPanelForced] = 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);
@@ -593,11 +659,19 @@ const Room = () => {
const handleFileUpload = async (files: FileList) => {
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";
for (let i = 0; i < files.length; 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 {
// Create form data for file upload
const formData = new FormData();
@@ -651,8 +725,8 @@ const Room = () => {
}
};
const showPopup = (message: string) => {
setPopupMessage(message);
const showPopup = (message: string, type: 'default' | 'warning' = 'default') => {
setPopupMessage({text: message, type});
setTimeout(() => setPopupMessage(null), 2000);
};
@@ -668,8 +742,8 @@ const Room = () => {
<div
className="absolute inset-0 transition-all duration-300"
style={{
left: showLeftPanel ? '320px' : '0px',
right: showRightPanel ? '320px' : '0px',
left: isMobile ? '0px' : (showLeftPanel ? '320px' : '0px'),
right: isMobile ? '0px' : (showRightPanel ? '320px' : '0px'),
}}
>
<div
@@ -762,6 +836,38 @@ const Room = () => {
{getThemeById(currentThemeId)?.name || "Switch theme"}
</HoverCardContent>
</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 className="flex-grow flex flex-col p-1 w-full">
@@ -770,15 +876,25 @@ const Room = () => {
{error}
</div>
)}
<CodeEditor
ref={editorRef}
value={content}
onChange={handleContentChange}
onSelectionChange={handleSelectionChange}
language="plaintext"
className="flex-grow w-full"
themeConfig={getThemeById(currentThemeId)}
/>
{isMobile ? (
<textarea
value={content}
onChange={(e) => handleContentChange(e.target.value)}
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"
placeholder="Start typing your code here..."
style={{ fontFamily: 'JetBrains Mono, Consolas, Monaco, "Courier New", monospace' }}
/>
) : (
<CodeEditor
ref={editorRef}
value={content}
onChange={handleContentChange}
onSelectionChange={handleSelectionChange}
language="plaintext"
className="flex-grow w-full"
themeConfig={getThemeById(currentThemeId)}
/>
)}
</div>
</div>
</div>
@@ -801,7 +917,7 @@ const Room = () => {
{/* Comments Panel */}
<CommentsPanel
isVisible={showRightPanel}
isVisible={isMobile ? rightPanelForced : showRightPanel}
onToggle={() => setRightPanelForced(!rightPanelForced)}
selectedLineStart={selectedLineStart}
selectedLineEnd={selectedLineEnd}
@@ -814,8 +930,12 @@ const Room = () => {
{/* Custom Popup */}
{popupMessage && (
<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">
<span className="text-sm font-medium">{popupMessage}</span>
<div className={`px-3 py-2 border rounded-lg shadow-lg animate-in fade-in slide-in-from-top-2 duration-200 ${
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>
)}
@@ -833,7 +953,7 @@ const Room = () => {
{/* Left Panel (Users, Media & ECG) */}
<LeftPanel
isVisible={showLeftPanel}
isVisible={isMobile ? leftPanelForced : showLeftPanel}
users={users}
mediaFiles={mediaFiles}
onFileUpload={handleFileUpload}

View File

@@ -113,6 +113,7 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
const [playingAudio, setPlayingAudio] = useState<string | 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 }>({});
// Helper function to get the correct file URL using HTTP server
@@ -202,9 +203,30 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
setPlayingAudio(null);
} else {
if (!audioRefs.current[fileId]) {
audioRefs.current[fileId] = new Audio(url);
audioRefs.current[fileId].addEventListener('ended', () => {
const audio = new Audio(url);
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);
setAudioProgress(prev => ({
...prev,
[fileId]: { currentTime: 0, duration: audio.duration }
}));
});
}
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) => {
// Call parent delete handler if available
if (onFileDelete) {
@@ -240,7 +276,7 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
return (
<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'
}`}
>
@@ -423,21 +459,52 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
)}
{file.type.startsWith('audio/') && (
<div className="mt-2 flex items-center space-x-2">
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
onClick={() => handlePlayAudio(file.id, getFileUrl(file))}
>
{playingAudio === file.id ? (
<Pause size={12} />
) : (
<Play size={12} />
)}
</Button>
<div className="flex-1 h-2 bg-muted rounded-full">
<div className="h-2 bg-primary rounded-full w-0"></div>
<div className="mt-2 space-y-2">
<div className="flex items-center space-x-2">
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
onClick={() => handlePlayAudio(file.id, getFileUrl(file))}
>
{playingAudio === file.id ? (
<Pause size={12} />
) : (
<Play size={12} />
)}
</Button>
<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>
)}

View File

@@ -109,7 +109,7 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
return (
<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'
}`}
>