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

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

Loading…
Cancel
Save