Files
KLC-Hr-Dashboard-Frontend/src/pages/DiscussionsPage/DiscussionsPage.tsx
priyanshuvish b7fa790d6e
All checks were successful
Build-Check / Build and Test PR (pull_request) Successful in 38s
Sonar Check / SonarQube Scan (pull_request) Successful in 59s
discussion change
2026-04-22 14:54:14 +05:30

318 lines
16 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { Button } from '../../components/ui/button';
import { Card, CardContent } from '../../components/ui/card';
import { Input } from '../../components/ui/input';
import { Textarea } from '../../components/ui/textarea';
import { Badge } from '../../components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../components/ui/dialog';
import { AlertCircle, Calendar, MessageCircle, Plus, Search, User, X } from 'lucide-react';
import { useCreateThreadMutation, useGetThreadsQuery } from '../../redux/services/forumApi';
import { useToast } from '../../components/toast/useToast';
import DiscussionsView from './DiscussionsView';
const DiscussionsPage: React.FC = () => {
const { showToast } = useToast();
const [showNewThreadModal, setShowNewThreadModal] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [tagsFilter, setTagsFilter] = useState('all');
const [newThreadTitle, setNewThreadTitle] = useState('');
const [newThreadContent, setNewThreadContent] = useState('');
const [tagInput, setTagInput] = useState('');
const [newThreadTags, setNewThreadTags] = useState<string[]>([]);
const [createError, setCreateError] = useState('');
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);
const { data: threadsResponse, isLoading: threadsLoading, isFetching: threadsFetching } =
useGetThreadsQuery();
const [createThread, { isLoading: isCreatingThread }] = useCreateThreadMutation();
const threads = threadsResponse?.data ?? [];
const allTags = useMemo(() => {
const set = new Set<string>();
threads.forEach((thread) => {
thread.tags.forEach((tag) => set.add(tag));
});
return Array.from(set).sort((a, b) => a.localeCompare(b));
}, [threads]);
const filteredThreads = useMemo(() => {
return threads.filter((thread) => {
const query = searchTerm.trim().toLowerCase();
const matchesSearch =
!query ||
thread.title.toLowerCase().includes(query) ||
thread.content.toLowerCase().includes(query) ||
thread.tags.some((tag) => tag.toLowerCase().includes(query));
const matchesTag = tagsFilter === 'all' || thread.tags.includes(tagsFilter);
return matchesSearch && matchesTag;
});
}, [threads, searchTerm, tagsFilter]);
const selectedThread = useMemo(
() => threads.find((thread) => thread.id === selectedThreadId) ?? null,
[threads, selectedThreadId]
);
const getThreadReactionCount = (thread: (typeof threads)[number]) =>
thread.reactions.reduce((sum, r) => sum + r.count, 0);
const formatDate = (date: string) => {
const d = new Date(date);
return d.toLocaleDateString('en-GB');
};
const resetModal = () => {
setNewThreadTitle('');
setNewThreadContent('');
setTagInput('');
setNewThreadTags([]);
setCreateError('');
};
const addTag = (rawTag: string) => {
const value = rawTag.trim().toLowerCase();
if (!value) return;
if (newThreadTags.includes(value)) return;
setNewThreadTags((prev) => [...prev, value]);
};
const handleTagKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag(tagInput);
setTagInput('');
}
};
const handleCreateThread = async () => {
setCreateError('');
if (!newThreadTitle.trim() || !newThreadContent.trim()) {
setCreateError('Title and content are required.');
return;
}
try {
const response = await createThread({
title: newThreadTitle.trim(),
content: newThreadContent.trim(),
tags: newThreadTags,
}).unwrap();
showToast('Thread created', response.message || 'Thread created successfully.', 'success');
setShowNewThreadModal(false);
resetModal();
} catch (error: any) {
const message = error?.data?.message || 'Failed to create thread.';
setCreateError(message);
showToast('Create failed', message, 'error');
}
};
return (
<div className="space-y-4">
{selectedThread ? (
<DiscussionsView thread={selectedThread} onBack={() => setSelectedThreadId(null)} />
) : (
<>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-4xl font-bold tracking-tight">Discussion Forums</h1>
<p className="text-muted-foreground">Connect, share, and learn with your cohort members</p>
</div>
<Button onClick={() => setShowNewThreadModal(true)} className="min-tap-44">
<Plus className="h-4 w-4 mr-2" />
New Thread
</Button>
</div>
<Card>
<CardContent className="py-4">
<div className="flex items-center gap-3 flex-nowrap">
<Select defaultValue="leadership-development-q4-2024">
<SelectTrigger className="w-[300px] shrink-0">
<SelectValue placeholder="Programme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="leadership-development-q4-2024">Leadership Development Q4 2024</SelectItem>
</SelectContent>
</Select>
<div className="relative min-w-0 flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-9"
placeholder="Search threads..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ paddingLeft: '30px' }}
/>
</div>
<Select value={tagsFilter} onValueChange={setTagsFilter}>
<SelectTrigger className="w-[170px] shrink-0">
<SelectValue placeholder="All Tags" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tags</SelectItem>
{allTags.map((tag) => (
<SelectItem key={tag} value={tag}>
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="py-4 space-y-4">
<div>
<h2 className="text-2xl font-semibold">Discussions ({filteredThreads.length})</h2>
<p className="text-muted-foreground">Leadership Development Q4 2024 30 members</p>
</div>
{(threadsLoading || threadsFetching) && (
<div className="text-sm text-muted-foreground">Loading threads...</div>
)}
{!threadsLoading && filteredThreads.length === 0 && (
<div className="rounded-md border p-6 text-sm text-muted-foreground">No threads found.</div>
)}
<div className="space-y-3">
{filteredThreads.map((thread) => (
<button
key={thread.id}
type="button"
className="w-full rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm transition hover:bg-slate-50 cursor-pointer"
onClick={() => setSelectedThreadId(thread.id)}
style={{borderRadius: '12px'}}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-muted-foreground">
L
</div>
<div className="min-w-0 flex-1 border-l-[3px] border-[#0a2f6f] pl-3">
<div className="flex items-center gap-2">
<h3 className="truncate text-3xl font-semibold leading-tight">{thread.title}</h3>
</div>
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground">{thread.content}</p>
<div className="mt-3 flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<User className="h-4 w-4" />
Learner
</span>
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{formatDate(thread.created_at)}
</span>
<span className="flex items-center gap-1">
<MessageCircle className="h-4 w-4" />
{thread.reactions.length}
</span>
</div>
<div className="mt-2 flex flex-wrap gap-2">
{thread.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
</div>
</button>
))}
</div>
</CardContent>
</Card>
</>
)}
<Dialog
open={showNewThreadModal}
onOpenChange={(open: boolean) => {
setShowNewThreadModal(open);
if (!open) resetModal();
}}
>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Start New Discussion</DialogTitle>
<DialogDescription>Create a new thread for this forum.</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{createError && (
<div className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
<AlertCircle className="h-4 w-4" />
{createError}
</div>
)}
<div>
<label className="mb-2 block text-sm font-medium">Title *</label>
<Input
value={newThreadTitle}
onChange={(e) => setNewThreadTitle(e.target.value)}
placeholder="What would you like to discuss?"
maxLength={150}
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Content *</label>
<Textarea
value={newThreadContent}
onChange={(e) => setNewThreadContent(e.target.value)}
placeholder="Share your thoughts..."
className="min-h-[140px]"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Tags</label>
<Input
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleTagKeyDown}
placeholder="Type a tag and press Enter"
/>
{newThreadTags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{newThreadTags.map((tag) => (
<Badge key={tag} variant="secondary" className="gap-1">
{tag}
<button
type="button"
onClick={() =>
setNewThreadTags((prev) => prev.filter((existingTag) => existingTag !== tag))
}
aria-label={`Remove ${tag}`}
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
</div>
<div className="flex gap-2 pt-2">
<Button
className="flex-1"
onClick={handleCreateThread}
disabled={isCreatingThread || !newThreadTitle.trim() || !newThreadContent.trim()}
>
{isCreatingThread ? 'Creating...' : 'Create Thread'}
</Button>
<Button variant="outline" className="flex-1" onClick={() => setShowNewThreadModal(false)}>
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
};
export default DiscussionsPage;