From a361d483e2ed8941499211c3b947602b978a5fd6 Mon Sep 17 00:00:00 2001 From: nethmalgunawardhana Date: Tue, 22 Jul 2025 13:45:36 +0530 Subject: [PATCH 01/25] feat: implement view tracking and increment functionality for posts --- src/app/api/posts/[id]/views/route.ts | 132 +++++++++++++++++++ src/components/communityForum/postDetail.tsx | 102 +++++++------- 2 files changed, 179 insertions(+), 55 deletions(-) diff --git a/src/app/api/posts/[id]/views/route.ts b/src/app/api/posts/[id]/views/route.ts index e2f7cfb..38219c9 100644 --- a/src/app/api/posts/[id]/views/route.ts +++ b/src/app/api/posts/[id]/views/route.ts @@ -2,6 +2,29 @@ import { NextRequest, NextResponse } from 'next/server'; import connectToDatabase from '@/lib/db'; import Post from '@/lib/models/postSchema'; import mongoose from 'mongoose'; +import PostView from '@/lib/models/PostView'; +import jwt from 'jsonwebtoken'; + +// Helper function to get user ID from token +function getUserIdFromToken(req: NextRequest): string | null { + try { + const authHeader = req.headers.get('authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return null; + } + + const token = authHeader.split(' ')[1]; + if (!token) { + return null; + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: string }; + return decoded.userId; + } catch (error) { + console.error('Error decoding token:', error); + return null; + } +} // GET handler for fetching current view count of a post export async function GET(request: NextRequest) { @@ -41,3 +64,112 @@ export async function GET(request: NextRequest) { ); } } + +// POST handler for incrementing view count +export async function POST(request: NextRequest) { + try { + // Extract post ID from the URL path + const url = request.url; + const pathParts = url.split('/'); + const postId = pathParts[pathParts.length - 2]; // views is the last part, so -2 for postId + + // Validate MongoDB ObjectId + if (!mongoose.Types.ObjectId.isValid(postId)) { + return NextResponse.json( + { error: 'Invalid post ID format' }, + { status: 400 } + ); + } + + // Get user ID from token + const userId = getUserIdFromToken(request); + if (!userId) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Get forum ID from request body + const body = await request.json(); + const { forumId } = body; + + if (!forumId || !mongoose.Types.ObjectId.isValid(forumId)) { + return NextResponse.json( + { error: 'Invalid forum ID' }, + { status: 400 } + ); + } + + await connectToDatabase(); + + // Check if this is a new view or a recent duplicate + const now = new Date(); + const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); + + const existingView = await PostView.findOne({ + postId, + userId, + viewedAt: { $gt: fiveMinutesAgo } + }); + + // Only increment view count if this is not a recent duplicate view + if (!existingView) { + // Update or create post view record + await PostView.findOneAndUpdate( + { postId, userId }, + { + postId, + userId, + forumId, + viewedAt: now, + deviceType: 'desktop', // Default value + isComplete: false + }, + { upsert: true } + ); + + // Increment view count on the post + const post = await Post.findByIdAndUpdate( + postId, + { $inc: { views: 1 } }, + { new: true } + ).select('views'); + + if (!post) { + return NextResponse.json( + { error: 'Post not found' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + views: post.views, + message: 'View count incremented successfully' + }); + } else { + // Return current view count without incrementing + const post = await Post.findById(postId).select('views'); + + if (!post) { + return NextResponse.json( + { error: 'Post not found' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + views: post.views, + message: 'Recent view already recorded' + }); + } + } catch (error) { + console.error('Error updating post views:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/components/communityForum/postDetail.tsx b/src/components/communityForum/postDetail.tsx index 89f5ff5..a0d0e8a 100644 --- a/src/components/communityForum/postDetail.tsx +++ b/src/components/communityForum/postDetail.tsx @@ -81,46 +81,16 @@ const PostDetail = () => { const editQuillRef = useRef(null); const viewTrackedRef = useRef(false); - // Function to track post view - const trackPostView = useCallback(async (postId: string, forumId: string, timeSpent: number = 0, isComplete: boolean = false) => { - if (!user || !token) return; - - try { - const response = await fetch('/api/user/interactions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ - postId, - forumId, - interactionType: 'view', - deviceType: /Mobi|Android/i.test(navigator.userAgent) ? 'mobile' : 'desktop', - timeSpent, - isComplete - }) - }); + // Update the useEffect for view tracking + useEffect(() => { + // Reset view tracking when post changes + viewTrackedRef.current = false; + }, [postId]); - if (response.ok) { - console.log('Post view tracked successfully'); - - // Only update view count optimistically for initial views (timeSpent === 0) - if (timeSpent === 0) { - setPost(prevPost => { - if (prevPost && prevPost._id === postId) { - return { ...prevPost, views: (prevPost.views || 0) + 1 }; - } - return prevPost; - }); - } - } else { - console.error('Failed to track post view'); - } - } catch (error) { - console.error('Error tracking post view:', error); - } - }, [user, token]); + // Track initial post view - separate useEffect to avoid infinite loop + useEffect(() => { + // This is now handled by the dedicated API endpoint in another useEffect + }, []); // Fetch post details useEffect(() => { @@ -156,9 +126,6 @@ const PostDetail = () => { setEditImagePreview(getImageUrl(postData.post.imageUrl)); } - // Track post view if user is logged in - // Removed from here to prevent infinite loop - // Fetch replies with better error handling try { const repliesResponse = await fetch(`/api/posts/${postId}/replies`); @@ -188,19 +155,44 @@ const PostDetail = () => { fetchPostDetails(); }, [postId]); - // Track initial post view - separate useEffect to avoid infinite loop - useEffect(() => { - if (post && user && token && forumId && !loading && !viewTrackedRef.current) { - // Only track the initial view once when the post is loaded - viewTrackedRef.current = true; - trackPostView(post._id, forumId); - } - }, [post, user, token, forumId, loading, trackPostView]); - - // Reset view tracking when post changes + // Add a separate API call to increment view count useEffect(() => { - viewTrackedRef.current = false; - }, [postId]); + const incrementViewCount = async () => { + if (!postId || !user || !token || !forumId || viewTrackedRef.current) return; + + try { + viewTrackedRef.current = true; + + // Call the API to update view count in the database + const response = await fetch('/api/posts/' + postId + '/views', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ forumId }) + }); + + if (response.ok) { + const data = await response.json(); + // Update the post with the new view count from the server + setPost(prevPost => { + if (prevPost) { + return { ...prevPost, views: data.views }; + } + return prevPost; + }); + console.log('View count updated successfully:', data.views); + } else { + console.error('Failed to update view count'); + } + } catch (error) { + console.error('Error updating view count:', error); + } + }; + + incrementViewCount(); + }, [postId, user, token, forumId]); // Track time spent on post for analytics useEffect(() => { @@ -290,7 +282,7 @@ const PostDetail = () => { }).catch(err => console.error('Error tracking final time spent:', err)); } }; - }, [post, user, token, forumId]); // Remove trackPostView from dependencies + }, [post, user, token, forumId]); // Removed trackPostView from dependencies // Cleanup image preview URL when component unmounts useEffect(() => { From cc94a3ae386dcb7e9d3a2656bbadcd387827f143 Mon Sep 17 00:00:00 2001 From: AdeepaK2 Date: Tue, 22 Jul 2025 13:52:35 +0530 Subject: [PATCH 02/25] fix: add optional chaining to user ID references for improved safety and prevent runtime errors feat: implement early return in CompletionRequestModal for invalid session handling --- src/app/session/[sessionId]/page.tsx | 12 ++++----- .../sessionSystem/CompletionRequestModal.tsx | 11 +++++--- src/components/sessionTabs/OverviewTab.tsx | 10 +++---- src/components/sessionTabs/ProgressTab.tsx | 2 +- src/utils/systemApiAuth.ts | 26 +++++++++++++++++++ 5 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 src/utils/systemApiAuth.ts diff --git a/src/app/session/[sessionId]/page.tsx b/src/app/session/[sessionId]/page.tsx index 9878439..40c0638 100644 --- a/src/app/session/[sessionId]/page.tsx +++ b/src/app/session/[sessionId]/page.tsx @@ -152,7 +152,7 @@ export default function SessionWorkspace() { if (!session) return; try { - const otherUserId = session.user1Id._id === currentUserId ? session.user2Id._id : session.user1Id._id; + const otherUserId = session.user1Id?._id === currentUserId ? session.user2Id?._id : session.user1Id?._id; const response = await fetch(`/api/users/profile?id=${otherUserId}`); const data = await response.json(); @@ -413,11 +413,11 @@ export default function SessionWorkspace() { ); } - const otherUser = session.user1Id._id === currentUserId ? session.user2Id : session.user1Id; - const mySkill = session.user1Id._id === currentUserId ? session.skill1Id : session.skill2Id; - const otherSkill = session.user1Id._id === currentUserId ? session.skill2Id : session.skill1Id; - const myDescription = session.user1Id._id === currentUserId ? session.descriptionOfService1 : session.descriptionOfService2; - const otherDescription = session.user1Id._id === currentUserId ? session.descriptionOfService2 : session.descriptionOfService1; + const otherUser = session.user1Id?._id === currentUserId ? session.user2Id : session.user1Id; + const mySkill = session.user1Id?._id === currentUserId ? session.skill1Id : session.skill2Id; + const otherSkill = session.user1Id?._id === currentUserId ? session.skill2Id : session.skill1Id; + const myDescription = session.user1Id?._id === currentUserId ? session.descriptionOfService1 : session.descriptionOfService2; + const otherDescription = session.user1Id?._id === currentUserId ? session.descriptionOfService2 : session.descriptionOfService1; // Get proper display name for other user const getOtherUserName = () => { diff --git a/src/components/sessionSystem/CompletionRequestModal.tsx b/src/components/sessionSystem/CompletionRequestModal.tsx index 111330e..28b3625 100644 --- a/src/components/sessionSystem/CompletionRequestModal.tsx +++ b/src/components/sessionSystem/CompletionRequestModal.tsx @@ -101,9 +101,14 @@ export default function CompletionRequestModal({ if (!isOpen) return null; + // Early return if session is not provided or is invalid + if (!session || !session.user1Id || !session.user2Id) { + return null; + } + // Calculate statistics - const myWorks = works.filter(w => w.provideUser._id === currentUserId); - const otherWorks = works.filter(w => w.provideUser._id !== currentUserId); + const myWorks = works.filter(w => w.provideUser && w.provideUser._id === currentUserId); + const otherWorks = works.filter(w => w.provideUser && w.provideUser._id !== currentUserId); const myAcceptedWorks = myWorks.filter(w => w.acceptanceStatus === 'accepted').length; const otherAcceptedWorks = otherWorks.filter(w => w.acceptanceStatus === 'accepted').length; @@ -114,7 +119,7 @@ export default function CompletionRequestModal({ const averageProgress = Math.round((myProgressPercentage + otherProgressPercentage) / 2); // Get other user info - const otherUser = session.user1Id._id === currentUserId ? session.user2Id : session.user1Id; + const otherUser = session?.user1Id?._id === currentUserId ? session?.user2Id : session?.user1Id; const otherUserName = otherUser?.firstName ? `${otherUser.firstName} ${otherUser.lastName || ''}`.trim() : 'Other participant'; // Check if completion looks ready diff --git a/src/components/sessionTabs/OverviewTab.tsx b/src/components/sessionTabs/OverviewTab.tsx index 21e21a9..10477a4 100644 --- a/src/components/sessionTabs/OverviewTab.tsx +++ b/src/components/sessionTabs/OverviewTab.tsx @@ -138,11 +138,11 @@ export default function OverviewTab({ } }; - const otherUser = session.user1Id._id === currentUserId ? session.user2Id : session.user1Id; - const mySkill = session.user1Id._id === currentUserId ? session.skill1Id : session.skill2Id; - const otherSkill = session.user1Id._id === currentUserId ? session.skill2Id : session.skill1Id; - const myDescription = session.user1Id._id === currentUserId ? session.descriptionOfService1 : session.descriptionOfService2; - const otherDescription = session.user1Id._id === currentUserId ? session.descriptionOfService2 : session.descriptionOfService1; + const otherUser = session.user1Id?._id === currentUserId ? session.user2Id : session.user1Id; + const mySkill = session.user1Id?._id === currentUserId ? session.skill1Id : session.skill2Id; + const otherSkill = session.user1Id?._id === currentUserId ? session.skill2Id : session.skill1Id; + const myDescription = session.user1Id?._id === currentUserId ? session.descriptionOfService1 : session.descriptionOfService2; + const otherDescription = session.user1Id?._id === currentUserId ? session.descriptionOfService2 : session.descriptionOfService1; // Enhanced completion request handler const handleEnhancedCompletionRequest = () => { diff --git a/src/components/sessionTabs/ProgressTab.tsx b/src/components/sessionTabs/ProgressTab.tsx index 08d3df0..b9c2991 100644 --- a/src/components/sessionTabs/ProgressTab.tsx +++ b/src/components/sessionTabs/ProgressTab.tsx @@ -115,7 +115,7 @@ export default function ProgressTab({ // Work submissions works.forEach(work => { - const submitter = work.provideUser._id === currentUserId ? 'You' : getOtherUserName(); + const submitter = work.provideUser?._id === currentUserId ? 'You' : getOtherUserName(); events.push({ date: new Date(work.provideDate), type: 'work_submission', diff --git a/src/utils/systemApiAuth.ts b/src/utils/systemApiAuth.ts new file mode 100644 index 0000000..81f30f4 --- /dev/null +++ b/src/utils/systemApiAuth.ts @@ -0,0 +1,26 @@ +/** + * Utility functions for System API authentication + */ + +/** + * Create headers with system API key for internal API calls + * @returns Headers object with system API key authentication + */ +export function createSystemApiHeaders(): HeadersInit { + return { + 'Content-Type': 'application/json', + 'x-api-key': process.env.NEXT_PUBLIC_SYSTEM_API_KEY!, + }; +} + +/** + * Create headers with system API key for server-side API calls + * (Uses server-side environment variable) + * @returns Headers object with system API key authentication + */ +export function createServerSystemApiHeaders(): HeadersInit { + return { + 'Content-Type': 'application/json', + 'x-api-key': process.env.SYSTEM_API_KEY!, + }; +} From 4c9966b962da94e350c6924f67cdd905be7efd24 Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Tue, 22 Jul 2025 14:03:10 +0530 Subject: [PATCH 03/25] feat: add skill verification feature with navigation and filtering options --- src/app/dashboard/page.tsx | 2 +- .../User/DashboardContent/MySkillsContent.tsx | 174 ++++++++++++++++-- src/types/userSkill.ts | 1 + 3 files changed, 156 insertions(+), 21 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 32b00e5..5e7cac5 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -82,7 +82,7 @@ function UserDashboardPageContent() { /> ); case 'myskill': - return ; + return setActiveComponent('skillVerify')} />; case 'listings': return setActiveComponent('myskill')} />; case 'matches': diff --git a/src/components/User/DashboardContent/MySkillsContent.tsx b/src/components/User/DashboardContent/MySkillsContent.tsx index 9185b6c..08f0820 100644 --- a/src/components/User/DashboardContent/MySkillsContent.tsx +++ b/src/components/User/DashboardContent/MySkillsContent.tsx @@ -2,6 +2,7 @@ 'use client'; import React, { useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; import { getUserSkills, deleteUserSkill } from '@/services/skillService'; import { getSkillsUsedInMatches } from '@/services/trendingService'; import { UserSkill } from '@/types/userSkill'; @@ -9,10 +10,15 @@ import { useToast } from '@/lib/context/ToastContext'; import AddSkillForm from '@/components/Dashboard/skills/AddSkillForm'; import EditSkillForm from '@/components/Dashboard/skills/EditSkillForm'; import ConfirmationModal from '@/components/Dashboard/listings/ConfirmationModal'; -import { Info, AlertTriangle, Users, Calendar, Search, Filter, BarChart3, Award, Target, Activity, BookOpen, Settings, TrendingUp, Layers, Eye, Edit2, Trash2, Lock, ChevronDown } from 'lucide-react'; +import { Info, AlertTriangle, Users, Calendar, Search, Filter, BarChart3, Award, Target, Activity, BookOpen, Settings, TrendingUp, Layers, Eye, Edit2, Trash2, Lock, ChevronDown, BadgeCheck, AlertCircle, Clock, ExternalLink } from 'lucide-react'; -const SkillsPage = () => { +interface MySkillsContentProps { + onNavigateToSkillVerification?: () => void; +} + +const SkillsPage = ({ onNavigateToSkillVerification }: MySkillsContentProps = {}) => { const { showToast } = useToast(); + const router = useRouter(); // Fallback for standalone usage const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(true); const [showAddForm, setShowAddForm] = useState(false); @@ -32,12 +38,14 @@ const SkillsPage = () => { const [selectedCategory, setSelectedCategory] = useState('all'); const [selectedProficiency, setSelectedProficiency] = useState('all'); const [selectedUsageStatus, setSelectedUsageStatus] = useState('all'); + const [selectedVerificationStatus, setSelectedVerificationStatus] = useState('all'); const [sortBy, setSortBy] = useState('name'); // Custom dropdown states for mobile const [showCategoryDropdown, setShowCategoryDropdown] = useState(false); const [showProficiencyDropdown, setShowProficiencyDropdown] = useState(false); const [showUsageDropdown, setShowUsageDropdown] = useState(false); + const [showVerificationDropdown, setShowVerificationDropdown] = useState(false); const [showSortDropdown, setShowSortDropdown] = useState(false); // Function to fetch user skills and used skill IDs @@ -165,6 +173,8 @@ const SkillsPage = () => { const usedInListings = skills.filter(s => isSkillUsedInListing(s.id)).length; const usedInMatches = skills.filter(s => isSkillUsedInMatches(s.id)).length; const categories = [...new Set(skills.map(s => s.categoryName))].length; + const verifiedSkills = skills.filter(s => s.isVerified).length; + const unverifiedSkills = skills.filter(s => !s.isVerified).length; return { total: totalSkills, @@ -173,7 +183,9 @@ const SkillsPage = () => { beginner: beginnerSkills, usedInListings, usedInMatches, - categories + categories, + verified: verifiedSkills, + unverified: unverifiedSkills }; }, [skills, usedSkillIds, matchUsedSkills]); @@ -211,6 +223,12 @@ const SkillsPage = () => { if (selectedUsageStatus === 'matches' && !usedInMatch) return false; } + // Verification status filter + if (selectedVerificationStatus !== 'all') { + if (selectedVerificationStatus === 'verified' && !skill.isVerified) return false; + if (selectedVerificationStatus === 'unverified' && skill.isVerified) return false; + } + return true; }); @@ -234,7 +252,7 @@ const SkillsPage = () => { }); return filtered; - }, [skills, searchTerm, selectedCategory, selectedProficiency, selectedUsageStatus, sortBy, usedSkillIds, matchUsedSkills]); + }, [skills, searchTerm, selectedCategory, selectedProficiency, selectedUsageStatus, selectedVerificationStatus, sortBy, usedSkillIds, matchUsedSkills]); // Group filtered skills by category const skillsByCategory = filteredAndSortedSkills.reduce((acc, skill) => { @@ -275,6 +293,17 @@ const SkillsPage = () => { setViewingSkill(skill); }; + // Navigate to skill verification page + const navigateToSkillVerification = () => { + if (onNavigateToSkillVerification) { + // Use dashboard navigation if available + onNavigateToSkillVerification(); + } else { + // Fallback to router navigation for standalone usage + router.push('/user/skillverification'); + } + }; + // Truncate text const truncateText = (text: string, maxLength: number) => { if (text.length <= maxLength) return text; @@ -310,6 +339,7 @@ const SkillsPage = () => { setShowCategoryDropdown(false); setShowProficiencyDropdown(false); setShowUsageDropdown(false); + setShowVerificationDropdown(false); setShowSortDropdown(false); }; @@ -409,9 +439,25 @@ const SkillsPage = () => {
{/* Title row with proficiency level always inline */}
-

- {skill.skillTitle} -

+
+

+ {skill.skillTitle} +

+ {skill.isVerified ? ( + + ) : ( +
+ + +
+ )} +
{ { value: 'matches', label: 'In Matches' } ]; + // Verification status options + const verificationOptions = [ + { value: 'all', label: 'All Verification' }, + { value: 'verified', label: 'Verified' }, + { value: 'unverified', label: 'Unverified' } + ]; + // Sort options const sortOptions = [ { value: 'name', label: 'Sort by Name' }, @@ -541,7 +594,7 @@ const SkillsPage = () => { {/* Overall Statistics */} {skills.length > 0 && ( -
+
@@ -596,6 +649,29 @@ const SkillsPage = () => {
In Listings
+
+
+ +
+
{skillStats.verified}
+
Verified
+
+ + +
@@ -609,7 +685,7 @@ const SkillsPage = () => { {/* Search and Filters */} {skills.length > 0 && (
-
+
{/* Search - Full width on mobile */}
@@ -681,19 +757,10 @@ const SkillsPage = () => {
-
+
+ +
+
{ isOpen={showUsageDropdown} setIsOpen={setShowUsageDropdown} /> + +
+ + {/* Mobile sort filter row */} +
{ setSelectedCategory('all'); setSelectedProficiency('all'); setSelectedUsageStatus('all'); + setSelectedVerificationStatus('all'); closeAllDropdowns(); }} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" @@ -838,7 +934,14 @@ const SkillsPage = () => {
-

{viewingSkill.skillTitle}

+
+

{viewingSkill.skillTitle}

+ {viewingSkill.isVerified ? ( + + ) : ( + + )} +
{ {viewingSkill.categoryName}
+ {/* Verification status */} +
+
+ {viewingSkill.isVerified ? ( + <> + + Skill Verified + + ) : ( + <> + + Verification Needed + + )} +
+ {!viewingSkill.isVerified && ( + + )} +
+ {/* Status indicators */} {(isSkillUsedInListing(viewingSkill.id) || isSkillUsedInMatches(viewingSkill.id)) && (
diff --git a/src/types/userSkill.ts b/src/types/userSkill.ts index 8bbe4ae..ece27a2 100644 --- a/src/types/userSkill.ts +++ b/src/types/userSkill.ts @@ -9,6 +9,7 @@ export interface UserSkill { skillTitle: string; proficiencyLevel: 'Beginner' | 'Intermediate' | 'Expert'; description: string; + isVerified: boolean; createdAt: string; updatedAt?: string; } From e0f5a76a69028fd5f7c6095f6caa79dfbf0ccaef Mon Sep 17 00:00:00 2001 From: AdeepaK2 Date: Tue, 22 Jul 2025 14:14:10 +0530 Subject: [PATCH 04/25] feat: add functionality to check for pending counter offers before allowing new submissions --- src/components/sessionSystem/SessionCard.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/sessionSystem/SessionCard.tsx b/src/components/sessionSystem/SessionCard.tsx index b596cf9..93e7918 100644 --- a/src/components/sessionSystem/SessionCard.tsx +++ b/src/components/sessionSystem/SessionCard.tsx @@ -152,6 +152,13 @@ export default function SessionCard({ return sessionCounterOffers.filter(co => co.status === 'accepted').length; }; + const hasUserSentPendingCounterOffer = (sessionId: string) => { + const sessionCounterOffers = counterOffers[sessionId] || []; + return sessionCounterOffers.some(co => + co.status === 'pending' && co.counterOfferedBy._id === userId + ); + }; + const renderCounterOffer = (counterOffer: CounterOffer) => { return (
diff --git a/src/components/Dashboard/matches/MatchDetailsModal.tsx b/src/components/Dashboard/matches/MatchDetailsModal.tsx index 6355eac..1b1bbf3 100644 --- a/src/components/Dashboard/matches/MatchDetailsModal.tsx +++ b/src/components/Dashboard/matches/MatchDetailsModal.tsx @@ -431,7 +431,7 @@ const MatchDetailsModal: React.FC = ({ match, currentUse {(otherUserKycStatus === 'Accepted' || otherUserKycStatus === 'Approved') ? ( ) : ( - + )}

Partner Profile

diff --git a/src/components/User/DashboardContent/MatchesContent.tsx b/src/components/User/DashboardContent/MatchesContent.tsx index 8c7a3ea..3f87758 100644 --- a/src/components/User/DashboardContent/MatchesContent.tsx +++ b/src/components/User/DashboardContent/MatchesContent.tsx @@ -506,15 +506,13 @@ const MatchesPage = () => { {['pending', 'accepted', 'rejected'].map(status => { const statusMatches = matchesToShow.filter(match => match.status === status); if (statusMatches.length === 0) return null; - const statusConfig = { pending: { title: 'Pending Matches', icon: Clock, color: 'text-yellow-600', bg: 'bg-yellow-50', border: 'border-yellow-200' }, accepted: { title: 'Active Matches', icon: CheckCircle, color: 'text-green-600', bg: 'bg-green-50', border: 'border-green-200' }, rejected: { title: 'Declined Matches', icon: XCircle, color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' } }[status]; - + if (!statusConfig) return null; const StatusIcon = statusConfig.icon; - return (
diff --git a/src/components/User/DashboardContent/MySkillsContent.tsx b/src/components/User/DashboardContent/MySkillsContent.tsx index 08f0820..9c20cdb 100644 --- a/src/components/User/DashboardContent/MySkillsContent.tsx +++ b/src/components/User/DashboardContent/MySkillsContent.tsx @@ -93,14 +93,14 @@ const SkillsPage = ({ onNavigateToSkillVerification }: MySkillsContentProps = {} }, [fetchUserData]); // Check if a skill is used in a listing - const isSkillUsedInListing = (skillId: string) => { + const isSkillUsedInListing = React.useCallback((skillId: string) => { return usedSkillIds.includes(skillId); - }; + }, [usedSkillIds]); // Check if a skill is used in active matches - const isSkillUsedInMatches = (skillId: string) => { + const isSkillUsedInMatches = React.useCallback((skillId: string) => { return matchUsedSkills?.usedSkillIds?.includes(skillId) || false; - }; + }, [matchUsedSkills]); // Get match details for a skill const getSkillMatchDetails = (skillTitle: string) => { @@ -187,7 +187,7 @@ const SkillsPage = ({ onNavigateToSkillVerification }: MySkillsContentProps = {} verified: verifiedSkills, unverified: unverifiedSkills }; - }, [skills, usedSkillIds, matchUsedSkills]); + }, [skills, isSkillUsedInListing, isSkillUsedInMatches]); // Get unique categories for filter dropdown const categories = useMemo(() => { @@ -252,7 +252,7 @@ const SkillsPage = ({ onNavigateToSkillVerification }: MySkillsContentProps = {} }); return filtered; - }, [skills, searchTerm, selectedCategory, selectedProficiency, selectedUsageStatus, selectedVerificationStatus, sortBy, usedSkillIds, matchUsedSkills]); + }, [skills, searchTerm, selectedCategory, selectedProficiency, selectedUsageStatus, selectedVerificationStatus, sortBy, isSkillUsedInListing, isSkillUsedInMatches]); // Group filtered skills by category const skillsByCategory = filteredAndSortedSkills.reduce((acc, skill) => { @@ -444,10 +444,10 @@ const SkillsPage = ({ onNavigateToSkillVerification }: MySkillsContentProps = {} {skill.skillTitle} {skill.isVerified ? ( - + ) : (
- +
{new Date(story.publishedAt).toLocaleDateString()}

- {story.rating && ( + {typeof story.rating === 'number' && (
{[...Array(5)].map((_, i) => ( ))} diff --git a/src/services/skillService.ts b/src/services/skillService.ts index 203793b..b0a52e0 100644 --- a/src/services/skillService.ts +++ b/src/services/skillService.ts @@ -77,6 +77,7 @@ export const getUserSkills = async (): Promise> => { skillTitle: item.skillTitle, proficiencyLevel: item.proficiencyLevel, description: item.description, + isVerified: typeof item.isVerified === 'boolean' ? item.isVerified : false, createdAt: item.createdAt, updatedAt: item.updatedAt })); @@ -128,6 +129,7 @@ export const addUserSkill = async (skillData: NewSkillData): Promise Date: Tue, 22 Jul 2025 14:32:49 +0530 Subject: [PATCH 06/25] feat: enhance post view tracking with visitor ID support and caching improvements --- src/app/api/forums/[id]/posts/route.ts | 18 ++++--- src/app/api/posts/[id]/views/route.ts | 50 +++++++++++++------- src/components/communityForum/ForumPosts.tsx | 48 ++++++++++--------- src/lib/models/PostView.ts | 14 ++++-- 4 files changed, 81 insertions(+), 49 deletions(-) diff --git a/src/app/api/forums/[id]/posts/route.ts b/src/app/api/forums/[id]/posts/route.ts index f49aed1..defd56e 100644 --- a/src/app/api/forums/[id]/posts/route.ts +++ b/src/app/api/forums/[id]/posts/route.ts @@ -16,7 +16,7 @@ export async function GET(request: NextRequest) { const limit = parseInt(searchParams.get('limit') || '10', 10); const page = parseInt(searchParams.get('page') || '1', 10); const skip = (page - 1) * limit; - + // Validate MongoDB ObjectId if (!mongoose.Types.ObjectId.isValid(forumId)) { return NextResponse.json( @@ -24,9 +24,9 @@ export async function GET(request: NextRequest) { { status: 400 } ); } - + await connectToDatabase(); - + const posts = await Post.find({ forumId, $or: [{ isDeleted: { $ne: true } }, { isDeleted: { $exists: false } }] @@ -34,12 +34,18 @@ export async function GET(request: NextRequest) { .sort({ createdAt: -1 }) .skip(skip) .limit(limit); - + const total = await Post.countDocuments({ forumId, $or: [{ isDeleted: { $ne: true } }, { isDeleted: { $exists: false } }] }); - + + // Add cache control headers to prevent caching + const headers = new Headers(); + headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + headers.set('Pragma', 'no-cache'); + headers.set('Expires', '0'); + return NextResponse.json({ posts, pagination: { @@ -48,7 +54,7 @@ export async function GET(request: NextRequest) { limit, pages: Math.ceil(total / limit), }, - }); + }, { headers }); } catch (error) { console.error('Error fetching forum posts:', error); return NextResponse.json( diff --git a/src/app/api/posts/[id]/views/route.ts b/src/app/api/posts/[id]/views/route.ts index 38219c9..33e162f 100644 --- a/src/app/api/posts/[id]/views/route.ts +++ b/src/app/api/posts/[id]/views/route.ts @@ -81,14 +81,13 @@ export async function POST(request: NextRequest) { ); } - // Get user ID from token - const userId = getUserIdFromToken(request); - if (!userId) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } + // Get user ID from token or generate a temporary ID from IP and user agent + let userId = getUserIdFromToken(request); + const ip = request.headers.get('x-forwarded-for') || 'unknown-ip'; + const userAgent = request.headers.get('user-agent') || 'unknown-agent'; + + // If no authenticated user, create a visitor ID from IP and user agent + const visitorId = userId || `visitor-${ip}-${userAgent.substring(0, 50)}`; // Get forum ID from request body const body = await request.json(); @@ -104,23 +103,36 @@ export async function POST(request: NextRequest) { await connectToDatabase(); // Check if this is a new view or a recent duplicate + // Reduced cooldown to 1 minute to allow more frequent view counts const now = new Date(); - const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); + const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000); const existingView = await PostView.findOne({ postId, - userId, - viewedAt: { $gt: fiveMinutesAgo } + $or: [ + { userId: visitorId }, + { visitorId: visitorId } + ], + viewedAt: { $gt: oneMinuteAgo } }); + let post; + // Only increment view count if this is not a recent duplicate view if (!existingView) { // Update or create post view record await PostView.findOneAndUpdate( - { postId, userId }, + { + postId, + $or: [ + { userId: visitorId }, + { visitorId: visitorId } + ] + }, { postId, - userId, + userId: userId || null, + visitorId: userId ? null : visitorId, forumId, viewedAt: now, deviceType: 'desktop', // Default value @@ -130,7 +142,7 @@ export async function POST(request: NextRequest) { ); // Increment view count on the post - const post = await Post.findByIdAndUpdate( + post = await Post.findByIdAndUpdate( postId, { $inc: { views: 1 } }, { new: true } @@ -146,11 +158,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, views: post.views, - message: 'View count incremented successfully' + message: 'View count incremented successfully', + updated: true }); } else { - // Return current view count without incrementing - const post = await Post.findById(postId).select('views'); + // Always fetch the latest view count from the database + post = await Post.findById(postId).select('views'); if (!post) { return NextResponse.json( @@ -162,7 +175,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, views: post.views, - message: 'Recent view already recorded' + message: 'Recent view already recorded', + updated: false }); } } catch (error) { diff --git a/src/components/communityForum/ForumPosts.tsx b/src/components/communityForum/ForumPosts.tsx index 2d45b06..a9a58ab 100644 --- a/src/components/communityForum/ForumPosts.tsx +++ b/src/components/communityForum/ForumPosts.tsx @@ -51,7 +51,13 @@ const ForumPosts: React.FC = ({ forumId }) => { const fetchPosts = useCallback(async () => { try { setLoading(true); - const response = await fetch(`/api/forums/${forumId}/posts`); + const response = await fetch(`/api/forums/${forumId}/posts`, { + // Add cache: 'no-store' to ensure we always get fresh data + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache' + } + }); if (!response.ok) { throw new Error('Failed to fetch forum posts'); @@ -59,6 +65,7 @@ const ForumPosts: React.FC = ({ forumId }) => { const data = await response.json(); setPosts(data.posts); + console.log('Fetched posts with updated view counts:', data.posts.map((p: Post) => ({ id: p._id, views: p.views }))); } catch (err) { console.error('Error fetching forum posts:', err); setError('Unable to load posts. Please try again later.'); @@ -173,31 +180,28 @@ const ForumPosts: React.FC = ({ forumId }) => { // Mark this post as clicked for visual feedback setClickedPosts(prev => new Set(prev).add(postId)); - // Optimistically update view count for better UX - if (user) { - handleViewUpdate(postId); + // Update view count via API + try { + const response = await fetch(`/api/posts/${postId}/views`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ forumId }), + }); - // Track view interaction for analytics (but don't duplicate the increment) - try { - await trackInteraction({ - postId, - forumId, - interactionType: 'view', - timeSpent: 0 - }); - } catch (error) { - console.error('Error tracking interaction:', error); - // Revert the optimistic update if tracking fails - setPosts(prevPosts => - prevPosts.map(post => - post._id === postId - ? { ...post, views: Math.max((post.views || 1) - 1, 0) } - : post - ) - ); + if (response.ok) { + const data = await response.json(); + if (data.updated) { + // Update view count locally if it was actually incremented + handleViewUpdate(postId); + } } + } catch (error) { + console.error('Error updating view count:', error); } + // Navigate to the post detail page router.push(`/forum/${forumId}/posts/${postId}`); }; diff --git a/src/lib/models/PostView.ts b/src/lib/models/PostView.ts index b28d093..f3ed912 100644 --- a/src/lib/models/PostView.ts +++ b/src/lib/models/PostView.ts @@ -3,7 +3,8 @@ import mongoose, { Document, Schema } from 'mongoose'; export interface IPostView extends Document { _id: string; postId: mongoose.Types.ObjectId; - userId: mongoose.Types.ObjectId; + userId?: mongoose.Types.ObjectId | null; + visitorId?: string | null; forumId: mongoose.Types.ObjectId; viewedAt: Date; timeSpent: number; // in seconds @@ -21,8 +22,13 @@ const PostViewSchema = new Schema({ userId: { type: Schema.Types.ObjectId, ref: 'User', - required: true, - index: true + index: true, + default: null + }, + visitorId: { + type: String, + index: true, + default: null }, forumId: { type: Schema.Types.ObjectId, @@ -54,7 +60,9 @@ const PostViewSchema = new Schema({ // Compound indexes for better query performance PostViewSchema.index({ userId: 1, postId: 1 }); +PostViewSchema.index({ visitorId: 1, postId: 1 }); PostViewSchema.index({ userId: 1, viewedAt: -1 }); +PostViewSchema.index({ visitorId: 1, viewedAt: -1 }); PostViewSchema.index({ postId: 1, viewedAt: -1 }); PostViewSchema.index({ forumId: 1, viewedAt: -1 }); From e27bbd09a779d7dbc48cda7ed2e149bd28024cbe Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Tue, 22 Jul 2025 15:29:38 +0530 Subject: [PATCH 07/25] feat: implement hero stats API endpoint and integrate stats fetching in EnhancedHeroSection --- src/app/api/stats/hero/route.ts | 52 ++++++++++++++++++++++++++ src/app/page.tsx | 66 +++++++++++++++++++++++++++++---- 2 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 src/app/api/stats/hero/route.ts diff --git a/src/app/api/stats/hero/route.ts b/src/app/api/stats/hero/route.ts new file mode 100644 index 0000000..1b74063 --- /dev/null +++ b/src/app/api/stats/hero/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from 'next/server'; +import dbConnect from '@/lib/db'; +import User from '@/lib/models/userSchema'; +import SkillList from '@/lib/models/skillList'; +import SkillMatch from '@/lib/models/skillMatch'; +import { Feedback } from '@/lib/models/feedbackSchema'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + await dbConnect(); + + // Active learners + const activeLearners = await User.countDocuments({ isDeleted: { $ne: true }, isBlocked: { $ne: true } }); + + // Skills available (unique skill names across all categories) + const skillLists = await SkillList.find({}).select('skills'); + const skillSet = new Set(); + skillLists.forEach(list => { + list.skills.forEach(skill => { + skillSet.add(skill.name); + }); + }); + const skillsAvailable = skillSet.size; + + // Successful matches and total matches + const successfulMatches = await SkillMatch.countDocuments({ status: 'completed' }); + const totalMatches = await SkillMatch.countDocuments({}); + + // Satisfaction rate (average rating) + const feedbacks = await Feedback.find({ rating: { $exists: true } }).select('rating'); + let satisfactionRate = 0; + if (feedbacks.length > 0) { + const avg = feedbacks.reduce((sum, f) => sum + (f.rating || 0), 0) / feedbacks.length; + satisfactionRate = Math.round(avg * 20); // Convert 1-5 to % + } + + return NextResponse.json({ + success: true, + data: { + activeLearners, + skillsAvailable, + successfulMatches, + totalMatches, + satisfactionRate + } + }); + } catch (error) { + return NextResponse.json({ success: false, message: 'Failed to fetch hero stats' }, { status: 500 }); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index d47117a..28784d4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,10 +25,24 @@ const EnhancedHeroSection = () => { const router = useRouter(); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [isVisible, setIsVisible] = useState(false); + const [stats, setStats] = useState<{ + activeLearners: number | null; + skillsAvailable: number | null; + successfulMatches: number | null; + totalMatches: number | null; + satisfactionRate: number | null; + }>({ + activeLearners: null, + skillsAvailable: null, + successfulMatches: null, + totalMatches: null, + satisfactionRate: null, + }); + const [loadingStats, setLoadingStats] = useState(true); useEffect(() => { setIsVisible(true); - + const handleMouseMove = (e: MouseEvent) => { setMousePosition({ x: (e.clientX / window.innerWidth) * 100, @@ -40,6 +54,25 @@ const EnhancedHeroSection = () => { return () => window.removeEventListener('mousemove', handleMouseMove); }, []); + useEffect(() => { + // Fetch hero stats from backend + const fetchStats = async () => { + try { + setLoadingStats(true); + const res = await fetch('/api/stats/hero'); + const data = await res.json(); + if (data.success && data.data) { + setStats(data.data); + } + } catch (err) { + // Optionally handle error + } finally { + setLoadingStats(false); + } + }; + fetchStats(); + }, []); + return (
{/* Animated Background Elements */} @@ -144,13 +177,32 @@ const EnhancedHeroSection = () => { {/* Stats */}
{[ - { number: '50K+', label: 'Active Learners', icon: Users }, - { number: '1000+', label: 'Skills Available', icon: BookOpen }, - { number: '25K+', label: 'Successful Matches', icon: Star }, - { number: '95%', label: 'Satisfaction Rate', icon: Award } + { + number: loadingStats || stats.activeLearners === null ? '...' : stats.activeLearners.toLocaleString(), + label: 'Active Learners', + icon: Users + }, + { + number: loadingStats || stats.skillsAvailable === null ? '...' : stats.skillsAvailable.toLocaleString(), + label: 'Skills Available', + icon: BookOpen + }, + { + number: + loadingStats || stats.totalMatches === null + ? '...' + : stats.totalMatches.toLocaleString(), + label: 'Total Matches', + icon: Star + }, + { + number: loadingStats || stats.satisfactionRate === null ? '...' : `${stats.satisfactionRate}%`, + label: 'Satisfaction Rate', + icon: Award + } ].map((stat, index) => ( -
From 337a9079bdfe941640f528cd65964c18f7428c6b Mon Sep 17 00:00:00 2001 From: AdeepaK2 Date: Tue, 22 Jul 2025 16:42:34 +0530 Subject: [PATCH 08/25] feat: final fix --- src/components/sessionSystem/SessionCard.tsx | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/sessionSystem/SessionCard.tsx b/src/components/sessionSystem/SessionCard.tsx index 93e7918..a83afad 100644 --- a/src/components/sessionSystem/SessionCard.tsx +++ b/src/components/sessionSystem/SessionCard.tsx @@ -159,6 +159,11 @@ export default function SessionCard({ ); }; + const hasAnyCounterOffer = (sessionId: string) => { + const sessionCounterOffers = counterOffers[sessionId] || []; + return sessionCounterOffers.length > 0; + }; + const renderCounterOffer = (counterOffer: CounterOffer) => { return (
); } - const displayName = `${user.firstName} ${user.lastName}`.trim(); + const displayName = `${user.firstName} ${user.lastName}`.trim(); // Helper function to update specific form field const updateField = ( @@ -162,7 +158,6 @@ export default function KYCForm() { setFormState(initialFormState); setFileError(null); setNicError(null); - // Clear file input elements const fileInputs = document.querySelectorAll( @@ -173,8 +168,6 @@ export default function KYCForm() { }); }; - - // Validate file size and type const validateFile = (file: File): string | null => { if (file.size > FILE_CONSTRAINTS.MAX_SIZE_BYTES) { @@ -188,10 +181,6 @@ export default function KYCForm() { return null; }; - - - - // Validate NIC format using regex patterns const validateNIC = (nicNumber: string): boolean => { return ( @@ -308,8 +297,7 @@ export default function KYCForm() { isError: true, }); return; - } - + } // Validate NIC format if (!validateNIC(formState.nic)) { @@ -359,7 +347,7 @@ export default function KYCForm() { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}`, + Authorization: `Bearer ${token}`, }, body: JSON.stringify({ userId: user!._id, @@ -413,6 +401,16 @@ export default function KYCForm() { return (
+ {/* Back button */} +
+ + + + +
{/* Responsive image container - hidden on mobile */}
@@ -434,7 +432,7 @@ export default function KYCForm() { {FORM_LABELS.TITLE} {/* Read-only Name field */} -
+
diff --git a/src/components/sessionTabs/OverviewTab.tsx b/src/components/sessionTabs/OverviewTab.tsx index 10477a4..b646133 100644 --- a/src/components/sessionTabs/OverviewTab.tsx +++ b/src/components/sessionTabs/OverviewTab.tsx @@ -759,15 +759,18 @@ export default function OverviewTab({ )} {cancelRequest.initiatorId._id === currentUserId && cancelRequest.responseStatus === 'disputed' && ( - <> - {/* Initiator can finalize after dispute */} - - +
+
+ +
+

Cancellation Disputed

+

+ The other party has disputed your cancellation request. Since no agreement could be reached, + please contact our support team for assistance in resolving this matter. +

+
+
+
)}
)} @@ -1015,9 +1018,9 @@ export default function OverviewTab({ {/* Session Cancellation Modal */} {showCancelModal && ( -
-
-
+
+
+

@@ -1032,19 +1035,21 @@ export default function OverviewTab({

- setShowCancelModal(false)} - /> +
+ setShowCancelModal(false)} + /> +
)} {/* Session Cancellation Response Modal */} {showCancelResponseModal && cancelRequest && ( -
-
-
+
+
+

@@ -1059,13 +1064,15 @@ export default function OverviewTab({

- setShowCancelResponseModal(false)} - initiatorName={getUserName(cancelRequest.initiatorId)} - reason={cancelRequest.reason} - description={cancelRequest.description} - /> +
+ setShowCancelResponseModal(false)} + initiatorName={getUserName(cancelRequest.initiatorId)} + reason={cancelRequest.reason} + description={cancelRequest.description} + /> +
)} From 666e9f20f79c188d334333b641634266e335b63c Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Tue, 22 Jul 2025 17:14:23 +0530 Subject: [PATCH 11/25] feat: add skill verification logic and integrate user skills fetching in MatchCard and MatchDetailsModal --- .../Dashboard/matches/MatchCard.tsx | 13 ++----- .../Dashboard/matches/MatchDetailsModal.tsx | 34 +++++++++++++++++-- .../User/DashboardContent/MatchesContent.tsx | 1 + src/services/skillServiceAdmin.ts | 27 +++++++++++++++ 4 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 src/services/skillServiceAdmin.ts diff --git a/src/components/Dashboard/matches/MatchCard.tsx b/src/components/Dashboard/matches/MatchCard.tsx index 215fb42..557969e 100644 --- a/src/components/Dashboard/matches/MatchCard.tsx +++ b/src/components/Dashboard/matches/MatchCard.tsx @@ -5,15 +5,16 @@ import Image from 'next/image'; import { SkillMatch } from '@/types/skillMatch'; import { BadgeCheck, ArrowRightLeft, Eye, MessageCircle, Clock, CheckCircle, XCircle, Award, Calendar, AlertCircle } from 'lucide-react'; import { processAvatarUrl } from '@/utils/avatarUtils'; +import { getUserSkillsByUserId } from '@/services/skillServiceAdmin'; interface MatchCardProps { match: SkillMatch; onClick: () => void; + currentUserId?: string; // Needed to look up myDetails userId } -const MatchCard: React.FC = ({ match, onClick }) => { +const MatchCard: React.FC = ({ match, onClick, currentUserId }) => { const [otherUserKycStatus, setOtherUserKycStatus] = useState(null); - // Fetch KYC status for the other user useEffect(() => { async function fetchKycStatus() { @@ -255,14 +256,6 @@ const MatchCard: React.FC = ({ match, onClick }) => { : "🔄 Partial match! They can teach you what you're seeking."}

- - {/* Success indicators */} - {match.status === 'accepted' && ( -
- - Chat available -
- )}
diff --git a/src/components/Dashboard/matches/MatchDetailsModal.tsx b/src/components/Dashboard/matches/MatchDetailsModal.tsx index 1b1bbf3..8dae1d2 100644 --- a/src/components/Dashboard/matches/MatchDetailsModal.tsx +++ b/src/components/Dashboard/matches/MatchDetailsModal.tsx @@ -10,6 +10,7 @@ import { fetchUserChatRooms } from '@/services/chatApiServices'; import { useAuth } from '@/lib/context/AuthContext'; import { BadgeCheck, ArrowRight, MessageCircle, Calendar, XCircle, CheckCircle, Clock, Award, BarChart3, Target, AlertCircle } from 'lucide-react'; import { processAvatarUrl } from '@/utils/avatarUtils'; +import { getUserSkillsByUserId } from '@/services/skillServiceAdmin'; interface MatchDetailsModalProps { match: SkillMatch; @@ -94,6 +95,31 @@ const MatchDetailsModal: React.FC = ({ match, currentUse const [openingChat, setOpeningChat] = useState(false); const [otherUserKycStatus, setOtherUserKycStatus] = useState(null); + // Skill verification state and effect (must be inside component) + const [mySkillVerified, setMySkillVerified] = useState(null); + const [otherSkillVerified, setOtherSkillVerified] = useState(null); + useEffect(() => { + async function fetchSkillVerification() { + // My skill: use currentUserId + if (currentUserId && match.myDetails && match.myDetails.offeringSkill) { + const res = await getUserSkillsByUserId(currentUserId); + if (res.success && res.data) { + const found = res.data.find((s: any) => s.skillTitle === match.myDetails.offeringSkill); + setMySkillVerified(found ? !!found.isVerified : false); + } + } + // Other user's skill + if (match.otherUser && match.otherUser.offeringSkill && match.otherUser.userId) { + const res = await getUserSkillsByUserId(match.otherUser.userId); + if (res.success && res.data) { + const found = res.data.find((s: any) => s.skillTitle === match.otherUser.offeringSkill); + setOtherSkillVerified(found ? !!found.isVerified : false); + } + } + } + fetchSkillVerification(); + }, [currentUserId, match.myDetails, match.otherUser]); + // Fetch KYC status for the other user useEffect(() => { async function fetchKycStatus() { @@ -371,8 +397,10 @@ const MatchDetailsModal: React.FC = ({ match, currentUse
Offering -

+

{match.myDetails.offeringSkill} + {mySkillVerified === true && } + {mySkillVerified === false && }

You'll teach this skill @@ -441,8 +469,10 @@ const MatchDetailsModal: React.FC = ({ match, currentUse

They Offer -

+

{match.otherUser.offeringSkill} + {otherSkillVerified === true && } + {otherSkillVerified === false && }

{match.otherUser.firstName} will teach you this diff --git a/src/components/User/DashboardContent/MatchesContent.tsx b/src/components/User/DashboardContent/MatchesContent.tsx index 3f87758..7e48f3a 100644 --- a/src/components/User/DashboardContent/MatchesContent.tsx +++ b/src/components/User/DashboardContent/MatchesContent.tsx @@ -527,6 +527,7 @@ const MatchesPage = () => { key={match.id} match={match} onClick={() => viewMatchDetails(match)} + currentUserId={currentUserId} /> ))}

diff --git a/src/services/skillServiceAdmin.ts b/src/services/skillServiceAdmin.ts new file mode 100644 index 0000000..fbf2139 --- /dev/null +++ b/src/services/skillServiceAdmin.ts @@ -0,0 +1,27 @@ +// Service to fetch another user's skills by userId (for admin or cross-user lookups) +import { ApiResponse, UserSkill } from '@/types/userSkill'; + +export const getUserSkillsByUserId = async (userId: string): Promise> => { + try { + const response = await fetch(`/api/userskillfetch?userId=${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const apiResponse = await response.json(); + if (apiResponse.success && apiResponse.categories) { + // Flatten all skills from all categories + const allSkills = apiResponse.categories.flatMap((cat: any) => cat.skills.map((s: any) => ({ + ...s, + categoryId: cat.categoryId, + categoryName: cat.categoryName, + }))); + return { success: true, data: allSkills }; + } + return { success: false, message: apiResponse.message || 'Failed to fetch user skills' }; + } catch (error) { + console.error('Error fetching user skills by userId:', error); + return { success: false, message: 'Failed to fetch user skills' }; + } +}; From c41e5bcea1586928712a99d54611a61dd0051d52 Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Tue, 22 Jul 2025 17:20:07 +0530 Subject: [PATCH 12/25] feat: center status display in MatchCard footer and remove quick action button --- .../Dashboard/matches/MatchCard.tsx | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/components/Dashboard/matches/MatchCard.tsx b/src/components/Dashboard/matches/MatchCard.tsx index 557969e..0c278b7 100644 --- a/src/components/Dashboard/matches/MatchCard.tsx +++ b/src/components/Dashboard/matches/MatchCard.tsx @@ -259,32 +259,15 @@ const MatchCard: React.FC = ({ match, onClick, currentUserId })
- {/* Card Footer */} + {/* Card Footer - Centered status only */}
-
- {/* Quick action based on status */} -
-
- {match.status === 'pending' && '⏳ Awaiting your response'} - {match.status === 'accepted' && '🚀 Ready to collaborate'} - {match.status === 'completed' && '✅ Successfully completed'} - {match.status === 'rejected' && '❌ Match declined'} -
+
+
+ {match.status === 'pending' && '⏳ Awaiting your response'} + {match.status === 'accepted' && '🚀 Ready to collaborate'} + {match.status === 'completed' && '✅ Successfully completed'} + {match.status === 'rejected' && '❌ Match declined'}
- -
From bc147a6a2ff5ff1dd47f0e3842c6c012dddac728 Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Tue, 22 Jul 2025 17:45:15 +0530 Subject: [PATCH 13/25] feat: add KYC status fetching for current user and display verification status in MatchDetailsModal --- .../Dashboard/matches/MatchDetailsModal.tsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/components/Dashboard/matches/MatchDetailsModal.tsx b/src/components/Dashboard/matches/MatchDetailsModal.tsx index 8dae1d2..77d76c5 100644 --- a/src/components/Dashboard/matches/MatchDetailsModal.tsx +++ b/src/components/Dashboard/matches/MatchDetailsModal.tsx @@ -98,6 +98,7 @@ const MatchDetailsModal: React.FC = ({ match, currentUse // Skill verification state and effect (must be inside component) const [mySkillVerified, setMySkillVerified] = useState(null); const [otherSkillVerified, setOtherSkillVerified] = useState(null); + const [myKycStatus, setMyKycStatus] = useState(null); useEffect(() => { async function fetchSkillVerification() { // My skill: use currentUserId @@ -120,21 +121,30 @@ const MatchDetailsModal: React.FC = ({ match, currentUse fetchSkillVerification(); }, [currentUserId, match.myDetails, match.otherUser]); - // Fetch KYC status for the other user + // Fetch KYC status for both users useEffect(() => { async function fetchKycStatus() { try { - const res = await fetch(`/api/kyc/status?userId=${match.otherUser.userId}`); - const data = await res.json(); - setOtherUserKycStatus(data.success ? data.status : null); + // Fetch other user's KYC status + const otherRes = await fetch(`/api/kyc/status?userId=${match.otherUser.userId}`); + const otherData = await otherRes.json(); + setOtherUserKycStatus(otherData.success ? otherData.status : null); + + // Fetch my KYC status + if (currentUserId) { + const myRes = await fetch(`/api/kyc/status?userId=${currentUserId}`); + const myData = await myRes.json(); + setMyKycStatus(myData.success ? myData.status : null); + } } catch (err) { setOtherUserKycStatus(null); + setMyKycStatus(null); } } if (match.otherUser.userId) { fetchKycStatus(); } - }, [match.otherUser.userId]); + }, [match.otherUser.userId, currentUserId]); // Format date const formatDate = (dateString: string) => { @@ -389,7 +399,14 @@ const MatchDetailsModal: React.FC = ({ match, currentUse />
-

Your Profile

+

+ Your Profile + {(myKycStatus === 'Accepted' || myKycStatus === 'Approved') ? ( + + ) : ( + + )} +

Skills Exchange

From 74d40fabb584636c847934808973d569513d75ee Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Tue, 22 Jul 2025 18:03:07 +0530 Subject: [PATCH 14/25] feat: add Contact Us section to homepage and update footer link --- src/app/page.tsx | 4 + src/components/homepage/ContactUsSection.tsx | 117 +++++++++++++++++++ src/components/homepage/Footer.tsx | 2 +- 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/components/homepage/ContactUsSection.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 28784d4..531f41f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import Chatbot from "@/components/chatassistant/chatbot"; import Footer from "@/components/homepage/Footer"; import SuccessStoriesCarousel from "@/components/homepage/SuccessStoriesCarousel"; import TrendingSkills from "@/components/homepage/TrendingSkills"; +import ContactUsSection from "@/components/homepage/ContactUsSection"; import { ArrowRight, Sparkles, @@ -243,6 +244,9 @@ export default function Home() { {/* Success Stories - Now with consistent blue theme */} + {/* Contact Us Section */} + + {/* Footer */}