mirror of
https://github.com/arkorty/Osborne.git
synced 2026-03-18 00:57:14 +00:00
style
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -45,6 +45,9 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
||||
}) => {
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [selectedLine, setSelectedLine] = useState<number | null>(null);
|
||||
const [scrollState, setScrollState] = useState({ top: false, bottom: false });
|
||||
|
||||
const commentsScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Update selected line when editor selection changes
|
||||
useEffect(() => {
|
||||
@@ -59,6 +62,36 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
||||
}
|
||||
}, [selectedLineStart, selectedLineEnd]);
|
||||
|
||||
// Scroll detection function
|
||||
const handleScroll = () => {
|
||||
const element = commentsScrollRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = element;
|
||||
const isScrolledFromTop = scrollTop > 5;
|
||||
const isScrolledFromBottom = scrollTop < scrollHeight - clientHeight - 5;
|
||||
|
||||
setScrollState({
|
||||
top: isScrolledFromTop,
|
||||
bottom: isScrolledFromBottom && scrollHeight > clientHeight
|
||||
});
|
||||
};
|
||||
|
||||
// Add scroll listener
|
||||
useEffect(() => {
|
||||
const element = commentsScrollRef.current;
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('scroll', handleScroll);
|
||||
// Initial check
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}
|
||||
}, [comments]);
|
||||
|
||||
const handleAddComment = () => {
|
||||
if (newComment.trim() && onAddComment && currentUser) {
|
||||
const lineRange = selectedLineStart && selectedLineEnd && selectedLineStart !== selectedLineEnd
|
||||
@@ -74,14 +107,19 @@ export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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">
|
||||
<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 ui-font ${
|
||||
isVisible ? 'transform-none' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
{/* Comments List */}
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
||||
<div
|
||||
ref={commentsScrollRef}
|
||||
className={`flex-1 overflow-y-auto hide-scrollbar scroll-shadow p-2 space-y-2 ${
|
||||
scrollState.top ? 'scroll-top' : ''
|
||||
} ${scrollState.bottom ? 'scroll-bottom' : ''}`}
|
||||
>
|
||||
{comments.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
<MessageSquare size={20} className="mx-auto mb-1 opacity-50" />
|
||||
|
||||
@@ -39,7 +39,6 @@ interface MediaFile {
|
||||
|
||||
interface LeftPanelProps {
|
||||
isVisible: boolean;
|
||||
isConnected: boolean;
|
||||
className?: string;
|
||||
users?: ActiveUser[];
|
||||
mediaFiles?: MediaFile[];
|
||||
@@ -50,7 +49,6 @@ interface LeftPanelProps {
|
||||
|
||||
export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
isVisible,
|
||||
className = '',
|
||||
users = [],
|
||||
mediaFiles = [],
|
||||
onFileDelete,
|
||||
@@ -58,12 +56,57 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
}) => {
|
||||
const [activeUsers, setActiveUsers] = useState<ActiveUser[]>(users);
|
||||
const [localMediaFiles, setLocalMediaFiles] = useState<MediaFile[]>(mediaFiles);
|
||||
const [usersScrollState, setUsersScrollState] = useState({ top: false, bottom: false });
|
||||
const [mediaScrollState, setMediaScrollState] = useState({ top: false, bottom: false });
|
||||
|
||||
const usersScrollRef = useRef<HTMLDivElement>(null);
|
||||
const mediaScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Update local state when props change
|
||||
useEffect(() => {
|
||||
setActiveUsers(users);
|
||||
}, [users]);
|
||||
|
||||
// Scroll detection function
|
||||
const handleScroll = (element: HTMLDivElement | null, setState: (state: { top: boolean; bottom: boolean }) => void) => {
|
||||
if (!element) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = element;
|
||||
const isScrolledFromTop = scrollTop > 5;
|
||||
const isScrolledFromBottom = scrollTop < scrollHeight - clientHeight - 5;
|
||||
|
||||
setState({
|
||||
top: isScrolledFromTop,
|
||||
bottom: isScrolledFromBottom && scrollHeight > clientHeight
|
||||
});
|
||||
};
|
||||
|
||||
// Add scroll listeners
|
||||
useEffect(() => {
|
||||
const usersElement = usersScrollRef.current;
|
||||
const mediaElement = mediaScrollRef.current;
|
||||
|
||||
const handleUsersScroll = () => handleScroll(usersElement, setUsersScrollState);
|
||||
const handleMediaScroll = () => handleScroll(mediaElement, setMediaScrollState);
|
||||
|
||||
if (usersElement) {
|
||||
usersElement.addEventListener('scroll', handleUsersScroll);
|
||||
// Initial check
|
||||
handleUsersScroll();
|
||||
}
|
||||
|
||||
if (mediaElement) {
|
||||
mediaElement.addEventListener('scroll', handleMediaScroll);
|
||||
// Initial check
|
||||
handleMediaScroll();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (usersElement) usersElement.removeEventListener('scroll', handleUsersScroll);
|
||||
if (mediaElement) mediaElement.removeEventListener('scroll', handleMediaScroll);
|
||||
};
|
||||
}, [activeUsers, localMediaFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalMediaFiles(mediaFiles);
|
||||
}, [mediaFiles]);
|
||||
@@ -195,15 +238,27 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-64 flex flex-col space-y-4 ${className}`}>
|
||||
<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 ${
|
||||
isVisible ? 'transform-none' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
{/* Users Panel */}
|
||||
<div className="bg-card border border-border rounded-md shadow-lg">
|
||||
<CardContent className="p-2 space-y-3 max-h-64 overflow-y-auto">
|
||||
<div className="h-1/2 flex flex-col border-b border-border">
|
||||
<div className="flex items-center justify-center py-2 border-b border-border/50 bg-muted/20">
|
||||
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||
<Users size={16} />
|
||||
Users
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={usersScrollRef}
|
||||
className={`flex-1 overflow-y-auto hide-scrollbar scroll-shadow p-2 space-y-2 ${
|
||||
usersScrollState.top ? 'scroll-top' : ''
|
||||
} ${usersScrollState.bottom ? 'scroll-bottom' : ''}`}
|
||||
>
|
||||
{activeUsers.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
<Users size={20} className="mx-auto mb-2 opacity-50" />
|
||||
@@ -282,9 +337,10 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
);
|
||||
})
|
||||
)}
|
||||
</CardContent>
|
||||
<div className="px-4 pb-3 border-t border-border bg-muted/50 rounded-b-md">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-3">
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3 border-t border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
{activeUsers.filter(u => getStatusIndicator(u).status === 'online').length} online
|
||||
</span>
|
||||
@@ -296,8 +352,20 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Media Panel */}
|
||||
<div className="bg-card border border-border rounded-md shadow-lg">
|
||||
<CardContent className="p-2 space-y-3 max-h-64 overflow-y-auto">
|
||||
<div className="h-1/2 flex flex-col">
|
||||
<div className="flex items-center justify-center py-2 border-b border-border/50 bg-muted/20">
|
||||
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||
<Upload size={16} />
|
||||
Media
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={mediaScrollRef}
|
||||
className={`flex-1 overflow-y-auto hide-scrollbar scroll-shadow p-2 space-y-2 ${
|
||||
mediaScrollState.top ? 'scroll-top' : ''
|
||||
} ${mediaScrollState.bottom ? 'scroll-bottom' : ''}`}
|
||||
>
|
||||
{localMediaFiles.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
<Upload size={20} className="mx-auto mb-2 opacity-50" />
|
||||
@@ -396,9 +464,10 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
<div className="px-4 pb-3 border-t border-border bg-muted/50 rounded-b-md">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-3">
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3 border-t border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{localMediaFiles.length} files</span>
|
||||
<span>
|
||||
{formatFileSize(localMediaFiles.reduce((total, file) => total + file.size, 0))} total
|
||||
|
||||
217
client/components/RightPanel.tsx
Normal file
217
client/components/RightPanel.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
lineNumber: number | null;
|
||||
lineRange?: string;
|
||||
author: string;
|
||||
authorId?: string;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
lastSeen: Date;
|
||||
isTyping?: boolean;
|
||||
currentLine?: number;
|
||||
}
|
||||
|
||||
interface CommentsPageProps {
|
||||
isVisible: boolean;
|
||||
onToggle: () => void;
|
||||
selectedLineStart?: number;
|
||||
selectedLineEnd?: number;
|
||||
onCommentSelect?: (lineNumber: number, lineRange?: string) => void;
|
||||
comments?: Comment[];
|
||||
onAddComment?: (content: string, lineNumber?: number, lineRange?: string) => void;
|
||||
currentUser?: User | null;
|
||||
}
|
||||
|
||||
export const CommentsPanel: React.FC<CommentsPageProps> = ({
|
||||
isVisible,
|
||||
selectedLineStart,
|
||||
selectedLineEnd,
|
||||
onCommentSelect,
|
||||
comments = [],
|
||||
onAddComment,
|
||||
currentUser
|
||||
}) => {
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [selectedLine, setSelectedLine] = useState<number | null>(null);
|
||||
const [scrollState, setScrollState] = useState({ top: false, bottom: false });
|
||||
|
||||
const commentsScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Update selected line when editor selection changes
|
||||
useEffect(() => {
|
||||
if (selectedLineStart && selectedLineEnd) {
|
||||
if (selectedLineStart === selectedLineEnd) {
|
||||
setSelectedLine(selectedLineStart);
|
||||
} else {
|
||||
setSelectedLine(selectedLineStart); // Use start line for range selections
|
||||
}
|
||||
} else {
|
||||
setSelectedLine(null);
|
||||
}
|
||||
}, [selectedLineStart, selectedLineEnd]);
|
||||
|
||||
// Scroll detection function
|
||||
const handleScroll = () => {
|
||||
const element = commentsScrollRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = element;
|
||||
const isScrolledFromTop = scrollTop > 5;
|
||||
const isScrolledFromBottom = scrollTop < scrollHeight - clientHeight - 5;
|
||||
|
||||
setScrollState({
|
||||
top: isScrolledFromTop,
|
||||
bottom: isScrolledFromBottom && scrollHeight > clientHeight
|
||||
});
|
||||
};
|
||||
|
||||
// Add scroll listener
|
||||
useEffect(() => {
|
||||
const element = commentsScrollRef.current;
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('scroll', handleScroll);
|
||||
// Initial check
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}
|
||||
}, [comments]);
|
||||
|
||||
const handleAddComment = () => {
|
||||
if (newComment.trim() && onAddComment && currentUser) {
|
||||
const lineRange = selectedLineStart && selectedLineEnd && selectedLineStart !== selectedLineEnd
|
||||
? `${selectedLineStart}-${selectedLineEnd}`
|
||||
: undefined;
|
||||
|
||||
onAddComment(newComment.trim(), selectedLine || undefined, lineRange);
|
||||
setNewComment('');
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
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 ${
|
||||
isVisible ? 'transform-none' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
{/* Comments List */}
|
||||
<div
|
||||
ref={commentsScrollRef}
|
||||
className={`flex-1 overflow-y-auto hide-scrollbar scroll-shadow p-2 space-y-2 ${
|
||||
scrollState.top ? 'scroll-top' : ''
|
||||
} ${scrollState.bottom ? 'scroll-bottom' : ''}`}
|
||||
>
|
||||
{comments.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
<MessageSquare size={20} className="mx-auto mb-1 opacity-50" />
|
||||
<p className="text-xs">No comments yet</p>
|
||||
<p className="text-xs">Add a comment to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
comments
|
||||
.sort((a, b) => {
|
||||
// Comments with line numbers come first, sorted by line number
|
||||
// Comments without line numbers come last
|
||||
if (a.lineNumber === null && b.lineNumber === null) return 0;
|
||||
if (a.lineNumber === null) return 1;
|
||||
if (b.lineNumber === null) return -1;
|
||||
return a.lineNumber - b.lineNumber;
|
||||
})
|
||||
.map((comment) => (
|
||||
<Card
|
||||
key={comment.id}
|
||||
className="border-border hover:shadow-md transition-shadow cursor-pointer"
|
||||
onClick={() => {
|
||||
if (comment.lineNumber && onCommentSelect) {
|
||||
onCommentSelect(comment.lineNumber, comment.lineRange);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardHeader className="pb-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{comment.author}
|
||||
</span>
|
||||
{comment.lineNumber !== null && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs bg-primary/10 text-primary hover:bg-primary/20 px-1 py-0"
|
||||
>
|
||||
{comment.lineRange || `Line ${comment.lineNumber}`}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatTime(comment.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<p className="text-xs text-foreground whitespace-pre-wrap">
|
||||
{comment.content}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Comment Form */}
|
||||
<div className="border-t border-border p-2 bg-muted/20">
|
||||
<div className="space-y-1">
|
||||
<Textarea
|
||||
placeholder={
|
||||
selectedLine
|
||||
? `Add a comment on ${selectedLineStart && selectedLineEnd && selectedLineStart !== selectedLineEnd
|
||||
? `lines ${selectedLineStart}-${selectedLineEnd}`
|
||||
: `line ${selectedLine}`
|
||||
}...`
|
||||
: "Add a general comment..."
|
||||
}
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
className="min-h-[60px] bg-background border-border text-foreground placeholder:text-muted-foreground resize-none text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleAddComment();
|
||||
}
|
||||
// Shift+Enter will naturally add a new line due to default behavior
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<kbd className="px-1 py-0 text-xs bg-muted border border-border rounded">
|
||||
Shift
|
||||
</kbd>
|
||||
{' + '}
|
||||
<kbd className="px-1 py-0 text-xs bg-muted border border-border rounded">
|
||||
Enter
|
||||
</kbd>
|
||||
{' for new line'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user