mirror of
https://github.com/arkorty/Osborne.git
synced 2026-03-17 16:51:44 +00:00
style again
This commit is contained in:
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user