Compare commits

...

2 Commits

  1. 244
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDiscussion.tsx
  2. 1
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseMedia.tsx

@ -8,28 +8,26 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { MessageCircle, Reply, X } from "lucide-react"; import { MessageCircle, Reply, X } from "lucide-react";
import { APP_BASE_URL } from '@/utils/constants'; import { APP_BASE_URL } from '@/utils/constants';
import { getEduConnectAccessToken } from '@/helpers/token.helper'; import { getEduConnectAccessToken } from '@/helpers/token.helper';
import { usePathname } from 'next/navigation';
// Types import { fetchHeader } from '@/helpers/fetch.helper';
interface Reply {
id: number; // Updated Types
user: string; interface Message {
avatar: string; id: string;
content: string; isSelf: number;
text: string;
timestamp: string; timestamp: string;
userId: string;
username: string;
} }
interface Discussion { interface ApiResponse {
id: number; count: number;
user: string; messages: Message[];
avatar: string;
content: string;
likes: number;
replies: Reply[];
timestamp: string;
} }
interface PageData { interface PageData {
discussions: Discussion[]; messages: Message[];
hasMore: boolean; hasMore: boolean;
} }
@ -39,20 +37,20 @@ interface DiscussionSectionProps {
// Key generator for SWR // Key generator for SWR
const getKey = (courseUuid: string) => (pageIndex: number, previousPageData: PageData | null) => { const getKey = (courseUuid: string) => (pageIndex: number, previousPageData: PageData | null) => {
if (previousPageData && !previousPageData.discussions.length) return null; if (previousPageData && !previousPageData.messages.length) return null;
return `${APP_BASE_URL}/api/chat/get?page=${pageIndex}`; return `${APP_BASE_URL}/api/chat/get`;
}; };
// Fetcher function // Updated fetcher function
const fetcher = async (url: string, courseUuid: string): Promise<PageData> => { const fetcher = async (url: string, courseUuid: string): Promise<PageData> => {
const formData = new FormData(); const formData = new FormData();
formData.append('course_uuid', courseUuid); formData.append('course_id', courseUuid);
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
body: formData , body: formData,
headers : { headers: {
Authorization : `Bearer ${getEduConnectAccessToken()}` Authorization: `Bearer ${getEduConnectAccessToken()}`
} }
}); });
@ -60,21 +58,22 @@ const fetcher = async (url: string, courseUuid: string): Promise<PageData> => {
throw new Error('Failed to fetch discussions'); throw new Error('Failed to fetch discussions');
} }
const data = await response.json(); const data: ApiResponse = await response.json();
return { return {
discussions: data.discussions.map((discussion: any) => ({ messages: data.messages.map(message => ({
...discussion, ...message,
timestamp: new Date(discussion.timestamp).toLocaleString() timestamp: new Date(message.timestamp).toLocaleString()
})), })),
hasMore: data.hasMore hasMore: data.count > data.messages.length
}; };
}; };
const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUuid }) => { const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUuid }) => {
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [replyingTo, setReplyingTo] = useState<{ commentId: number; userName: string } | null>(null); const [replyingTo, setReplyingTo] = useState<any>(null);
const loadingRef = useRef<HTMLDivElement>(null); const loadingRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const pathname = usePathname();
const id = pathname.split('/')?.at(-1);
const { const {
data, data,
@ -85,7 +84,7 @@ const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUui
mutate mutate
} = useSWRInfinite( } = useSWRInfinite(
getKey(courseUuid), getKey(courseUuid),
(url) => fetcher(url, courseUuid), (url) => fetcher(url, id!),
{ {
revalidateFirstPage: false, revalidateFirstPage: false,
revalidateOnFocus: false, revalidateOnFocus: false,
@ -93,7 +92,7 @@ const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUui
} }
); );
const allDiscussions = data ? data.flatMap(page => page.discussions) : []; const allMessages = data ? data.flatMap(page => page.messages) : [];
const hasMore = data ? data[data.length - 1]?.hasMore : true; const hasMore = data ? data[data.length - 1]?.hasMore : true;
const isLoadingMore = !data && !error || (size > 0 && data && typeof data[size - 1] === "undefined"); const isLoadingMore = !data && !error || (size > 0 && data && typeof data[size - 1] === "undefined");
@ -115,137 +114,79 @@ const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUui
return () => observer.disconnect(); return () => observer.disconnect();
}, [hasMore, isValidating, setSize, size]); }, [hasMore, isValidating, setSize, size]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (comment.trim()) { if (comment.trim()) {
const formData = new FormData(); const formData = new FormData();
formData.append('course_uuid', courseUuid); formData.append('course_id', courseUuid);
formData.append('content', comment); formData.append('message', comment);
if (replyingTo) { // if (replyingTo) {
formData.append('parent_id', replyingTo.commentId.toString()); // formData.append('parent_id', replyingTo.commentId.toString());
} // }
try {
const response = await fetch(`${APP_BASE_URL}/api/chat/send`, {
method: 'POST',
body: formData,
headers : fetchHeader()
});
try { if (!response.ok) {
const response = await fetch('/api/chat/post', { throw new Error('Failed to post comment');
method: 'POST', }
body: formData
});
if (!response.ok) {
throw new Error('Failed to post comment');
}
const newComment = await response.json(); // Optimistically update the UI
// Optimistically update the UI await mutate();
const updatedData = [...(data || [])]; setComment('');
if (replyingTo) { setReplyingTo(null);
// Update the discussion with the new reply } catch (error) {
updatedData.forEach(page => { console.error('Failed to post comment:', error);
page.discussions = page.discussions.map(discussion => { // You might want to show an error message to the user here
if (discussion.id === replyingTo.commentId) {
return {
...discussion,
replies: [...discussion.replies, {
...newComment,
timestamp: 'Just now'
}]
};
}
return discussion;
});
});
} else {
// Add new top-level comment
if (updatedData[0]) {
updatedData[0].discussions = [{
...newComment,
replies: [],
timestamp: 'Just now'
}, ...updatedData[0].discussions];
}
} }
await mutate(updatedData, false);
setComment('');
setReplyingTo(null);
} catch (error) {
console.error('Failed to post comment:', error);
// You might want to show an error message to the user here
} }
};
const handleReply = (commentId: number, userName: string) => {
setReplyingTo({ commentId, userName });
setComment(`@${userName} `);
};
const cancelReply = () => {
setReplyingTo(null);
setComment('');
};
if (error) {
return (
<div className="text-red-500 p-4 text-center">
Error loading discussions. Please try again later.
</div>
);
} }
};
const handleReply = (commentId: number, userName: string) => {
setReplyingTo({ commentId, userName });
setComment(`@${userName} `);
};
const cancelReply = () => {
setReplyingTo(null);
setComment('');
};
if (error) {
return (
<div className="text-red-500 p-4 text-center">
Error loading discussions. Please try again later.
</div>
);
}
// Rest of your component implementation remains similar, just update the rendering logic:
return ( return (
<div className="w-full max-w-3xl mx-auto"> <div className="w-full max-w-3xl mx-auto">
<ScrollArea className="h-[600px]"> <ScrollArea className="h-[600px]">
<div className="space-y-6 p-4"> <div className="space-y-6 p-4">
{allDiscussions.map((discussion) => ( {allMessages.map((message) => (
<Card key={discussion.id} className="p-4"> <Card key={message.id} className="p-4">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-4"> <div className="flex gap-4">
<Avatar className="w-8 h-8"> <Avatar className="w-8 h-8">
<AvatarImage src={discussion.avatar} alt={discussion.user} /> <AvatarImage src="/api/placeholder/32/32" alt={message.username} />
<AvatarFallback>{discussion.user[0]}</AvatarFallback> <AvatarFallback>{message.username[0]}</AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="font-medium">{discussion.user}</span> <span className="font-medium">{message.username}</span>
<span className="text-sm text-gray-500">{discussion.timestamp}</span> <span className="text-sm text-gray-500">{message.timestamp}</span>
</div>
<p className="text-gray-700 mb-2">{discussion.content}</p>
<div className="flex items-center gap-4">
<button
className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700"
onClick={() => handleReply(discussion.id, discussion.user)}
>
<Reply className="w-4 h-4" />
Reply
</button>
<div className="flex items-center gap-1 text-sm text-gray-500">
<MessageCircle className="w-4 h-4" />
{discussion.replies.length} replies
</div>
</div> </div>
<p className="text-gray-700 mb-2">{message.text}</p>
</div> </div>
</div> </div>
{discussion.replies.length > 0 && (
<div className="ml-12 space-y-4">
{discussion.replies.map((reply) => (
<div key={reply.id} className="flex gap-4">
<Avatar className="w-8 h-8">
<AvatarImage src={reply.avatar} alt={reply.user} />
<AvatarFallback>{reply.user[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{reply.user}</span>
<span className="text-sm text-gray-500">{reply.timestamp}</span>
</div>
<p className="text-gray-700">{reply.content}</p>
</div>
</div>
))}
</div>
)}
</div> </div>
</Card> </Card>
))} ))}
@ -254,8 +195,8 @@ const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUui
{isLoadingMore && ( {isLoadingMore && (
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900" /> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900" />
)} )}
{!hasMore && allDiscussions.length > 0 && ( {!hasMore && allMessages.length > 0 && (
<div className="text-gray-500">No more discussions</div> <div className="text-gray-500">No more messages</div>
)} )}
</div> </div>
</div> </div>
@ -268,26 +209,15 @@ const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUui
<AvatarFallback>U</AvatarFallback> <AvatarFallback>U</AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1"> <div className="flex-1">
{replyingTo && (
<div className="flex items-center gap-2 mb-2 text-sm text-gray-500">
<span>Replying to {replyingTo.userName}</span>
<button
onClick={cancelReply}
className="p-1 hover:bg-gray-100 rounded-full"
>
<X className="w-4 h-4" />
</button>
</div>
)}
<Textarea <Textarea
placeholder={replyingTo ? `Reply to ${replyingTo.userName}...` : "Add to the discussion..."} placeholder="Add to the discussion..."
value={comment} value={comment}
onChange={(e) => setComment(e.target.value)} onChange={(e) => setComment(e.target.value)}
className="min-h-24 mb-6" className="min-h-24 mb-6"
/> />
<div className="w-fit ml-auto"> <div className="w-fit ml-auto">
<Button onClick={handleSubmit} className="bg-purple-700"> <Button onClick={handleSubmit} className="bg-purple-700">
{replyingTo ? 'Post Reply' : 'Post Comment'} Post Message
</Button> </Button>
</div> </div>
</div> </div>

@ -10,6 +10,7 @@ import { useState } from "react"
const CourseMedia : React.FC<{courseData : Record<string,any>}> = ({courseData}) => { const CourseMedia : React.FC<{courseData : Record<string,any>}> = ({courseData}) => {
const [currentPage, setCurrentPage] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(0);
console.log(courseData)
return( return(
<div className="container mx-auto py-8"> <div className="container mx-auto py-8">

Loading…
Cancel
Save