Merge pull request 'discussion change' (#20) from priyanshu-dev into main
Reviewed-on: #20
This commit is contained in:
@@ -6,7 +6,7 @@ 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, MessageCircle, Plus, Search, ThumbsUp, X } from 'lucide-react';
|
||||
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';
|
||||
@@ -55,14 +55,6 @@ const DiscussionsPage: React.FC = () => {
|
||||
[threads, selectedThreadId]
|
||||
);
|
||||
|
||||
const getReactionCount = (emoji: string) =>
|
||||
(threadsResponse?.data ?? [])
|
||||
.flatMap((t) => t.reactions)
|
||||
.find(() => false) && 0;
|
||||
|
||||
const getThreadLikeCount = (thread: (typeof threads)[number]) =>
|
||||
thread.reactions.find((r) => r.emoji_code === 'U+1F44D')?.count ?? 0;
|
||||
|
||||
const getThreadReactionCount = (thread: (typeof threads)[number]) =>
|
||||
thread.reactions.reduce((sum, r) => sum + r.count, 0);
|
||||
|
||||
@@ -192,39 +184,41 @@ const DiscussionsPage: React.FC = () => {
|
||||
<button
|
||||
key={thread.id}
|
||||
type="button"
|
||||
className="w-full rounded-xl border bg-card p-4 text-left hover:bg-muted/20"
|
||||
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 justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-lg font-semibold">{thread.title}</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{thread.content}</p>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>By HR User</span>
|
||||
<span>•</span>
|
||||
<span>{formatDate(thread.created_at)}</span>
|
||||
<span>•</span>
|
||||
<span>Last activity {formatDate(thread.latest_activity)}</span>
|
||||
<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 bg-muted">
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
0
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
{getThreadLikeCount(thread)}
|
||||
</span>
|
||||
<span>{getThreadReactionCount(thread)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ArrowLeft, Ellipsis, Link2, List, RefreshCw, Send, Smile, ThumbsUp } from 'lucide-react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ArrowLeft, Ellipsis, MessageCircle, RefreshCw, Send, ThumbsUp } from 'lucide-react';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Card, CardContent } from '../../components/ui/card';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
@@ -28,12 +28,21 @@ const getLikeCount = (thread: ForumThread) =>
|
||||
thread.reactions.find((r) => r.emoji_code === 'U+1F44D')?.count ?? 0;
|
||||
|
||||
const getReactionCount = (thread: ForumThread) => thread.reactions.reduce((sum, r) => sum + r.count, 0);
|
||||
const reactionOptions = [
|
||||
{ emoji: 'U+1F44D', label: '👍' },
|
||||
{ emoji: 'U+1F602', label: '😂' },
|
||||
{ emoji: 'U+1F60D', label: '😍' },
|
||||
];
|
||||
|
||||
export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack }) => {
|
||||
const { showToast } = useToast();
|
||||
const [replyInput, setReplyInput] = useState('');
|
||||
const [childReplyInputById, setChildReplyInputById] = useState<Record<string, string>>({});
|
||||
const [openChildReplyForId, setOpenChildReplyForId] = useState<string | null>(null);
|
||||
const [openThreadReactionPicker, setOpenThreadReactionPicker] = useState(false);
|
||||
const [openReplyReactionPickerId, setOpenReplyReactionPickerId] = useState<string | null>(null);
|
||||
const threadReactionPickerRef = useRef<HTMLDivElement | null>(null);
|
||||
const replyReactionPickerRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
const { data: repliesResponse, isLoading: repliesLoading, refetch: refetchReplies } =
|
||||
useGetRepliesByThreadQuery(thread.id);
|
||||
@@ -54,10 +63,10 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
return countNodes(replies);
|
||||
}, [replies]);
|
||||
|
||||
const getReplyLikeCount = (reply: ForumReply) =>
|
||||
(reply.reactions ?? []).find((r) => r.emoji_code === 'U+1F44D')?.count ?? 0;
|
||||
const getReplyReactionCount = (reply: ForumReply) =>
|
||||
(reply.reactions ?? []).reduce((sum, r) => sum + r.count, 0);
|
||||
const getReactionLabel = (reactionCode: string) =>
|
||||
reactionOptions.find((opt) => opt.emoji === reactionCode)?.label ?? '🙂';
|
||||
|
||||
const postReply = async (content: string, parentId?: string) => {
|
||||
if (!content.trim()) return;
|
||||
@@ -81,33 +90,59 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
}
|
||||
};
|
||||
|
||||
const reactToThread = async () => {
|
||||
const reactToThread = async (emoji: string) => {
|
||||
try {
|
||||
await reactToForumItem({
|
||||
emoji: 'U+1F44D',
|
||||
emoji,
|
||||
thread_id: thread.id,
|
||||
}).unwrap();
|
||||
await refetchReplies();
|
||||
setOpenThreadReactionPicker(false);
|
||||
showToast('Reaction added', 'Your reaction was recorded.', 'success');
|
||||
} catch (error: any) {
|
||||
showToast('Reaction failed', error?.data?.message || 'Could not react right now.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const reactToReply = async (replyId: string) => {
|
||||
const reactToReply = async (replyId: string, emoji: string) => {
|
||||
try {
|
||||
await reactToForumItem({
|
||||
emoji: 'U+1F44D',
|
||||
thread_id: thread.id,
|
||||
emoji,
|
||||
reply_id: replyId,
|
||||
}).unwrap();
|
||||
await refetchReplies();
|
||||
setOpenReplyReactionPickerId(null);
|
||||
showToast('Reaction added', 'Your reaction was recorded.', 'success');
|
||||
} catch (error: any) {
|
||||
showToast('Reaction failed', error?.data?.message || 'Could not react right now.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleOutsideClick = (event: Event) => {
|
||||
const target = event.target as Node | null;
|
||||
if (!target) return;
|
||||
|
||||
if (openThreadReactionPicker && threadReactionPickerRef.current && !threadReactionPickerRef.current.contains(target)) {
|
||||
setOpenThreadReactionPicker(false);
|
||||
}
|
||||
|
||||
if (openReplyReactionPickerId) {
|
||||
const activeReplyRef = replyReactionPickerRefs.current[openReplyReactionPickerId];
|
||||
if (activeReplyRef && !activeReplyRef.contains(target)) {
|
||||
setOpenReplyReactionPickerId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', handleOutsideClick, true);
|
||||
document.addEventListener('touchstart', handleOutsideClick, true);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', handleOutsideClick, true);
|
||||
document.removeEventListener('touchstart', handleOutsideClick, true);
|
||||
};
|
||||
}, [openThreadReactionPicker, openReplyReactionPickerId]);
|
||||
|
||||
const renderReplies = (items: ForumReply[], depth = 0): React.ReactNode =>
|
||||
items.map((reply) => {
|
||||
const childReplyText = childReplyInputById[reply.id] ?? '';
|
||||
@@ -123,25 +158,49 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
<span className="font-medium text-foreground">HR User</span>
|
||||
<span>{formatDate(reply.created_at)}</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer">
|
||||
<Ellipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-3 text-sm">{reply.content}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{getReplyReactionCount(reply)} reactions</span>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<div
|
||||
className="relative"
|
||||
ref={(el) => {
|
||||
replyReactionPickerRefs.current[reply.id] = el;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => reactToReply(reply.id)}
|
||||
className="flex cursor-pointer items-center gap-1 outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 hover:text-foreground"
|
||||
onClick={() =>
|
||||
setOpenReplyReactionPickerId((prev) => (prev === reply.id ? null : reply.id))
|
||||
}
|
||||
disabled={reacting}
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
{getReplyLikeCount(reply)}
|
||||
<span>Like</span>
|
||||
</button>
|
||||
{openReplyReactionPickerId === reply.id && (
|
||||
<div className="absolute bottom-full left-0 z-30 mb-2 flex items-center gap-1 rounded-full border border-slate-200 bg-white p-1 shadow-xl">
|
||||
{reactionOptions.map((option) => (
|
||||
<button
|
||||
key={option.emoji + 'reply' + reply.id}
|
||||
type="button"
|
||||
className="cursor-pointer rounded-full p-1 text-lg leading-none transition hover:bg-muted"
|
||||
onClick={() => reactToReply(reply.id, option.emoji)}
|
||||
disabled={reacting}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span>{getReplyReactionCount(reply)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground"
|
||||
className="cursor-pointer hover:text-foreground"
|
||||
onClick={() => setOpenChildReplyForId(isChildBoxOpen ? null : reply.id)}
|
||||
>
|
||||
Reply
|
||||
@@ -174,7 +233,7 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Button variant="ghost" onClick={onBack} className="h-auto p-0 font-medium">
|
||||
<Button variant="ghost" onClick={onBack} className="h-auto cursor-pointer p-0 font-medium">
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
Back to Forums
|
||||
</Button>
|
||||
@@ -184,10 +243,10 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="space-y-4 py-6">
|
||||
<CardContent className="space-y-4 py-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 className="text-4xl font-semibold">{thread.title}</h2>
|
||||
<Button variant="ghost" size="icon">
|
||||
<h2 className="font-semibold leading-tight">{thread.title}</h2>
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer">
|
||||
<Ellipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -210,20 +269,34 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 border-t pt-4 text-sm text-muted-foreground">
|
||||
<span>{getReactionCount(thread)} 🤔</span>
|
||||
<div className="flex items-center gap-3 border-t pt-4 text-sm text-muted-foreground">
|
||||
<div className="relative" ref={threadReactionPickerRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reactToThread}
|
||||
className="flex cursor-pointer items-center gap-1 outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 hover:text-foreground"
|
||||
onClick={() => setOpenThreadReactionPicker((prev) => !prev)}
|
||||
disabled={reacting}
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
{getLikeCount(thread)}
|
||||
<span>Like</span>
|
||||
</button>
|
||||
<span className="flex items-center gap-1">
|
||||
<Smile className="h-4 w-4" />0
|
||||
</span>
|
||||
{openThreadReactionPicker && (
|
||||
<div className="absolute bottom-full left-0 z-30 mb-2 flex items-center gap-1 rounded-full border border-slate-200 bg-white p-1 shadow-xl">
|
||||
{reactionOptions.map((option) => (
|
||||
<button
|
||||
key={option.emoji + 'thread' + thread.id}
|
||||
type="button"
|
||||
className="cursor-pointer rounded-full p-1 text-2xl leading-none transition hover:bg-muted"
|
||||
onClick={() => reactToThread(option.emoji)}
|
||||
disabled={reacting}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span>{getReactionCount(thread)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -264,20 +337,7 @@ export const DiscussionsView: React.FC<DiscussionsViewProps> = ({ thread, onBack
|
||||
placeholder="Share your thoughts or ask a follow-up question..."
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* <div className="flex items-center gap-3 text-muted-foreground">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<span className="text-sm font-semibold">B</span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<span className="text-sm italic">I</span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Link2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="text-sm text-muted-foreground">Press Enter to post</div>
|
||||
<Button
|
||||
className="bg-[#7a78b0] hover:bg-[#69679d]"
|
||||
onClick={() => postReply(replyInput)}
|
||||
|
||||
@@ -98,17 +98,19 @@ const LearnersPage: React.FC = () => {
|
||||
const [editForm, setEditForm] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneCountryCode: '+1',
|
||||
phoneCountryCode: '+91',
|
||||
phoneNumber: '',
|
||||
});
|
||||
const [editSaveError, setEditSaveError] = useState('');
|
||||
const [assignedCourses, setAssignedCourses] = useState<AssignedCourse[]>([]);
|
||||
const [courseActionCourseId, setCourseActionCourseId] = useState<string | null>(null);
|
||||
const [programmeStartDate, setProgrammeStartDate] = useState('');
|
||||
const [programmeEndDate, setProgrammeEndDate] = useState('');
|
||||
const [newEmployee, setNewEmployee] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phoneCountryCode: '+1',
|
||||
phoneCountryCode: '+91',
|
||||
phone: '',
|
||||
});
|
||||
const [addLearnerError, setAddLearnerError] = useState('');
|
||||
@@ -707,11 +709,21 @@ const LearnersPage: React.FC = () => {
|
||||
setAssignError('Please select a programme.');
|
||||
return;
|
||||
}
|
||||
if (!programmeStartDate || !programmeEndDate) {
|
||||
setAssignError('Please select start date and end date.');
|
||||
return;
|
||||
}
|
||||
if (new Date(programmeEndDate) < new Date(programmeStartDate)) {
|
||||
setAssignError('End date cannot be before start date.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await bulkAssignProgramme({
|
||||
principal_xids: selectedEmployees,
|
||||
programme_xids: [selectedProgrammeId],
|
||||
start_date: programmeStartDate,
|
||||
end_date: programmeEndDate,
|
||||
}).unwrap();
|
||||
|
||||
showToast(
|
||||
@@ -721,6 +733,8 @@ const LearnersPage: React.FC = () => {
|
||||
);
|
||||
setShowAssignModal(false);
|
||||
setSelectedProgrammeId('');
|
||||
setProgrammeStartDate('');
|
||||
setProgrammeEndDate('');
|
||||
setSelectedEmployees([]);
|
||||
} catch (error: any) {
|
||||
const message = error?.data?.message || 'Failed to assign programme.';
|
||||
@@ -1468,6 +1482,26 @@ const LearnersPage: React.FC = () => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Start Date *</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={programmeStartDate}
|
||||
onChange={(e) => setProgrammeStartDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">End Date *</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={programmeEndDate}
|
||||
onChange={(e) => setProgrammeEndDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button
|
||||
className="flex-1"
|
||||
@@ -1482,6 +1516,8 @@ const LearnersPage: React.FC = () => {
|
||||
setShowAssignModal(false);
|
||||
setAssignError('');
|
||||
setSelectedProgrammeId('');
|
||||
setProgrammeStartDate('');
|
||||
setProgrammeEndDate('');
|
||||
}}
|
||||
className="flex-1"
|
||||
disabled={isAssigningProgramme}
|
||||
|
||||
@@ -35,7 +35,7 @@ const Login = () => {
|
||||
'linear-gradient(135deg, var(--accent-1), var(--accent-1), var(--accent-1))',
|
||||
}}
|
||||
>
|
||||
<Card className="w-full max-w-[450px] shadow-lg" style={{ maxWidth: '450px' }}>
|
||||
<Card className="w-full max-w-[450px] shadow-lg" style={{ maxWidth: '400px' }}>
|
||||
<CardHeader className="pb-2 text-center">
|
||||
<img
|
||||
src={klcLogo}
|
||||
|
||||
@@ -135,6 +135,8 @@ interface ProgrammeListResponse {
|
||||
interface BulkAssignProgrammeRequest {
|
||||
principal_xids: string[];
|
||||
programme_xids: string[];
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}
|
||||
|
||||
interface BulkAssignProgrammeResponse {
|
||||
|
||||
Reference in New Issue
Block a user