diff --git a/.github/workflows/badge-assignment.yml b/.github/workflows/badge-assignment.yml deleted file mode 100644 index ff759626..00000000 --- a/.github/workflows/badge-assignment.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Daily Badge Assignment - -on: - schedule: - # Runs daily at midnight UTC (adjust timezone as needed) - - cron: "0 0 * * *" - - # Allow manual trigger - workflow_dispatch: - -jobs: - assign-badges: - runs-on: ubuntu-latest - - steps: - - name: Call Badge Assignment API - run: | - curl -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \ - ${{ secrets.APP_URL }}/api/badge-assignments - env: - CRON_SECRET: ${{ secrets.CRON_SECRET }} - APP_URL: ${{ secrets.APP_URL }} diff --git a/package-lock.json b/package-lock.json index 2f679b9d..8b214b1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "dompurify": "^3.2.3", "dotenv": "^16.6.1", "formidable": "^3.5.2", - "framer-motion": "^11.15.0", + "framer-motion": "^11.18.2", "fuse.js": "^7.1.0", "googleapis": "^152.0.0", "jotai": "^2.12.5", @@ -8276,6 +8276,8 @@ }, "node_modules/framer-motion": { "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "license": "MIT", "dependencies": { "motion-dom": "^11.18.1", diff --git a/package.json b/package.json index 26544d61..c3ee55c5 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "dompurify": "^3.2.3", "dotenv": "^16.6.1", "formidable": "^3.5.2", - "framer-motion": "^11.15.0", + "framer-motion": "^11.18.2", "fuse.js": "^7.1.0", "googleapis": "^152.0.0", "jotai": "^2.12.5", diff --git a/src/components/meetingSystem/DailyMeeting.tsx b/src/components/meetingSystem/DailyMeeting.tsx index 4dcc51c0..c74ab422 100644 --- a/src/components/meetingSystem/DailyMeeting.tsx +++ b/src/components/meetingSystem/DailyMeeting.tsx @@ -26,9 +26,11 @@ import { Users, Maximize, Minimize, - StickyNote + StickyNote, + HelpCircle } from 'lucide-react'; import { MeetingNotesSidebar } from './MeetingNotesSidebar'; +import { MediaDeviceTips } from './MediaDeviceTips'; interface DailyMeetingProps { roomUrl: string; @@ -94,17 +96,32 @@ function DailyMeetingInner({ // UI State const [isFullscreen, setIsFullscreen] = useState(false); const [showNotes, setShowNotes] = useState(false); + const [showTips, setShowTips] = useState(false); const videoContainerRef = useRef(null); - // Meeting control functions + // Meeting control functions with enhanced error handling const toggleAudio = useCallback(async () => { if (!daily) return; - await daily.setLocalAudio(!localParticipant?.audio); + + try { + await daily.setLocalAudio(!localParticipant?.audio); + } catch (error) { + console.error('Audio toggle failed:', error); + // Show user-friendly error message + alert('Unable to access microphone. It might be in use by another application.'); + } }, [daily, localParticipant?.audio]); const toggleVideo = useCallback(async () => { if (!daily) return; - await daily.setLocalVideo(!localParticipant?.video); + + try { + await daily.setLocalVideo(!localParticipant?.video); + } catch (error) { + console.error('Video toggle failed:', error); + // Show user-friendly error message + alert('Unable to access camera. It might be in use by another browser tab or application.'); + } }, [daily, localParticipant?.video]); const toggleScreenShare = useCallback(async () => { @@ -133,6 +150,36 @@ function DailyMeetingInner({ } }, [daily, onLeave]); + // Function to join meeting with fallback options + const joinMeetingWithFallback = useCallback(async () => { + if (!daily) return; + + try { + // First try with camera and microphone + await daily.setLocalVideo(true); + await daily.setLocalAudio(true); + } catch (error) { + console.warn('Could not enable camera/microphone, trying audio-only mode:', error); + try { + // Fallback to audio-only if camera fails + await daily.setLocalVideo(false); + await daily.setLocalAudio(true); + } catch (audioError) { + console.warn('Audio also failed, joining in listen-only mode:', audioError); + // Last resort: join without any media + await daily.setLocalVideo(false); + await daily.setLocalAudio(false); + } + } + }, [daily]); + + // Call this when meeting state changes to joined + useEffect(() => { + if (meetingState === 'joined-meeting') { + joinMeetingWithFallback(); + } + }, [meetingState, joinMeetingWithFallback]); + // Toggle fullscreen const toggleFullscreen = useCallback(() => { if (!videoContainerRef.current) return; @@ -247,6 +294,14 @@ function DailyMeetingInner({
+
); } @@ -443,32 +504,146 @@ export default function DailyMeeting({ meetingDescription }: DailyMeetingProps) { const [callObject, setCallObject] = useState(null); + const [joinError, setJoinError] = useState(null); + const [isJoining, setIsJoining] = useState(true); useEffect(() => { + let isMounted = true; + import('@daily-co/daily-js').then((DailyIframe) => { - const call = DailyIframe.default.createCallObject(); + if (!isMounted) return; + + const call = DailyIframe.default.createCallObject({ + // Enhanced configuration for better device handling + videoSource: 'camera', + audioSource: 'microphone', + // Try to gracefully handle device conflicts + startVideoOff: false, + startAudioOff: false, + }); + + // Listen for join events + call.on('joined-meeting', () => { + if (isMounted) { + setIsJoining(false); + setJoinError(null); + } + }); + + call.on('error', (error: any) => { + if (isMounted) { + console.error('Daily meeting error:', error); + setIsJoining(false); + + // Handle specific error types + if (error.type === 'cam-in-use' || error.errorMsg?.includes('camera')) { + setJoinError('Camera is already in use by another tab or application. Please close other video calls and try again.'); + } else if (error.type === 'mic-in-use' || error.errorMsg?.includes('microphone')) { + setJoinError('Microphone is already in use by another tab or application. Please close other audio calls and try again.'); + } else if (error.errorMsg?.includes('Permission denied')) { + setJoinError('Camera and microphone access denied. Please allow permissions and refresh the page.'); + } else { + setJoinError('Failed to join meeting. Please try again or check your connection.'); + } + } + }); + setCallObject(call); - // Join the meeting + // Join the meeting with enhanced error handling call.join({ url: roomUrl, userName: userName || 'Anonymous User', }).catch((error: any) => { - console.error('Failed to join meeting:', error); + if (isMounted) { + console.error('Failed to join meeting:', error); + setIsJoining(false); + + // Parse error messages for better user feedback + const errorMessage = error.message || error.toString(); + if (errorMessage.includes('camera') || errorMessage.includes('video')) { + setJoinError('Camera access failed. Another browser tab might be using your camera. Please close other video calls and try again.'); + } else if (errorMessage.includes('microphone') || errorMessage.includes('audio')) { + setJoinError('Microphone access failed. Another application might be using your microphone.'); + } else if (errorMessage.includes('permission')) { + setJoinError('Please allow camera and microphone permissions to join the meeting.'); + } else { + setJoinError('Unable to connect to the meeting. Please check your internet connection and try again.'); + } + } }); return () => { - call.destroy(); + if (call && !call.isDestroyed()) { + call.destroy(); + } }; }); + + return () => { + isMounted = false; + }; }, [roomUrl, userName]); - if (!callObject) { + // Show error state with helpful message and retry option + if (joinError) { + return ( +
+
+
+
+ +
+

Unable to Join Meeting

+

{joinError}

+ +
+

💡 Quick Solutions:

+
    +
  • • Close other browser tabs using your camera/microphone
  • +
  • • Close video calling apps (Zoom, Teams, Skype, etc.)
  • +
  • • Refresh this page and allow camera permissions
  • +
  • • Try using a different browser or incognito mode
  • +
+
+
+ +
+ + + +
+
+
+ ); + } + + if (!callObject || isJoining) { return (
-

Initializing meeting...

+

+ {!callObject ? 'Initializing meeting...' : 'Connecting to meeting...'} +

+

+ Make sure no other tabs are using your camera or microphone +

); diff --git a/src/components/meetingSystem/MediaDeviceTips.tsx b/src/components/meetingSystem/MediaDeviceTips.tsx new file mode 100644 index 00000000..5b9a1674 --- /dev/null +++ b/src/components/meetingSystem/MediaDeviceTips.tsx @@ -0,0 +1,106 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle, X, Camera, Mic, Monitor, Smartphone } from 'lucide-react'; + +interface MediaDeviceTipsProps { + isVisible: boolean; + onClose: () => void; +} + +export function MediaDeviceTips({ isVisible, onClose }: MediaDeviceTipsProps) { + if (!isVisible) return null; + + return ( +
+
+
+ {/* Header */} +
+
+ +

Camera & Microphone Tips

+
+ +
+ + {/* Content */} +
+
+

⚠️ Multiple Browser Limitation

+

+ Only one browser tab can access your camera and microphone at the same time. + This is a security feature built into web browsers. +

+
+ +
+

If you can't access camera/microphone:

+ +
+
+ +
+

Close Other Browser Tabs

+

Close any other tabs that might be using your camera (video calls, other meetings)

+
+
+ +
+ +
+

Close Video Applications

+

Close apps like Zoom, Teams, Skype, or any camera software

+
+
+ +
+ +
+

Check Browser Permissions

+

Make sure you've allowed camera and microphone access for this website

+
+
+ +
+ +
+

Try Different Browser

+

If issues persist, try opening the meeting in a different browser or incognito mode

+
+
+
+
+ +
+

💡 Pro Tips:

+
    +
  • • Use one browser for video calls only
  • +
  • • Keep other video apps closed during meetings
  • +
  • • Refresh the page if you encounter permission issues
  • +
  • • Use headphones to prevent echo
  • +
+
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/src/components/meetingSystem/MediaDeviceWarning.tsx b/src/components/meetingSystem/MediaDeviceWarning.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/meetingSystem/MeetingTips.tsx b/src/components/meetingSystem/MeetingTips.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/messageSystem/MeetingBox.tsx b/src/components/messageSystem/MeetingBox.tsx index b5efcc4f..bf57c842 100644 --- a/src/components/messageSystem/MeetingBox.tsx +++ b/src/components/messageSystem/MeetingBox.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; import { Calendar, Plus, FileText, ChevronDown, ChevronRight } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; import CreateMeetingModal from '@/components/meetingSystem/CreateMeetingModal'; import CancelMeetingModal from '@/components/meetingSystem/CancelMeetingModal'; import MeetingList from '@/components/meetingSystem/MeetingList'; @@ -74,6 +75,80 @@ interface MeetingBoxProps { export default function MeetingBox({ chatRoomId, userId, onClose, onMeetingUpdate }: MeetingBoxProps) { const [meetings, setMeetings] = useState([]); const [loading, setLoading] = useState(true); + + // Animation variants + const containerVariants = { + hidden: { + opacity: 0, + y: 20, + scale: 0.95 + }, + visible: { + opacity: 1, + y: 0, + scale: 1, + transition: { + duration: 0.4, + ease: "easeOut", + staggerChildren: 0.1 + } + }, + exit: { + opacity: 0, + y: -20, + scale: 0.95, + transition: { + duration: 0.3, + ease: "easeIn" + } + } + }; + + const itemVariants = { + hidden: { + opacity: 0, + x: -20 + }, + visible: { + opacity: 1, + x: 0, + transition: { + duration: 0.3, + ease: "easeOut" + } + } + }; + + const headerVariants = { + hidden: { + opacity: 0, + y: -10 + }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + ease: "easeOut", + delay: 0.1 + } + } + }; + + const buttonHover = { + scale: 1.05, + transition: { + duration: 0.2, + ease: "easeInOut" + } + }; + + const buttonTap = { + scale: 0.95, + transition: { + duration: 0.1 + } + }; const [userProfiles, setUserProfiles] = useState({}); const [showCreateModal, setShowCreateModal] = useState(false); const [showCancelModal, setShowCancelModal] = useState(false); @@ -639,34 +714,167 @@ ${formattedContent} if (loading && meetings.length === 0) { return ( -
-
-

Loading meetings...

-
+ + {/* Background pulse effect */} + + +
+ + + Loading meetings... + +
+
); } return ( <> -
+ {/* Animated Background Elements */} + + {/* Floating circles */} + + + + + + {/* Header */} -
-
- + + + + +

Meetings

-
- -
+ +
{/* Meetings List */} -
+ setShowPastMeetings(!showPastMeetings)} onToggleCancelledMeetings={() => setShowCancelledMeetings(!showCancelledMeetings)} /> -
+
{/* Saved Meeting Notes - Collapsible */} -
-
- - {showSavedNotes && ( -
- -
- )} -
-
-
+ +
+ + {/* Modals */} - {showCreateModal && ( - setShowCreateModal(false)} - onCreate={handleCreateMeeting} - receiverName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'this user'} - /> - )} - - {showCancelModal && meetingToCancel && ( - { - setShowCancelModal(false); - setMeetingToCancel(null); - }} - onCancel={handleCancelMeeting} - userName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'User'} - /> - )} + + {showCreateModal && ( + + setShowCreateModal(false)} + onCreate={handleCreateMeeting} + receiverName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'this user'} + /> + + )} + + {showCancelModal && meetingToCancel && ( + + { + setShowCancelModal(false); + setMeetingToCancel(null); + }} + onCancel={handleCancelMeeting} + userName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'User'} + /> + + )} + {/* Alert Component */} {/* Notes View Modal */} - {showNotesModal && selectedNote && ( - { - setShowNotesModal(false); - setSelectedNote(null); - }} - onDownload={handleDownloadNotes} - /> - )} + + {showNotesModal && selectedNote && ( + + { + setShowNotesModal(false); + setSelectedNote(null); + }} + onDownload={handleDownloadNotes} + /> + + )} + ); } \ No newline at end of file diff --git a/src/components/messageSystem/MessageBox.tsx b/src/components/messageSystem/MessageBox.tsx index 0caafe77..ec69c77b 100644 --- a/src/components/messageSystem/MessageBox.tsx +++ b/src/components/messageSystem/MessageBox.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useEffect, useState, useRef, useCallback } from "react"; +import { motion, AnimatePresence } from 'framer-motion'; import { useSocket } from '@/lib/context/SocketContext'; import { IMessage } from "@/types/chat"; import { CornerUpLeft } from "lucide-react"; @@ -42,11 +43,20 @@ function DateBadge({ date }: { date: Date }) { }).format(date); return ( -
-
+ + {formattedDate} -
-
+ + ); } @@ -55,20 +65,43 @@ function DateBadge({ date }: { date: Date }) { */ function SkillMatchInfoMessage({ participantName }: { participantName?: string }) { return ( -
-
-
-
+ + + + New Skill Match! 🎉 -
-

+ + This chat was created because you and {participantName || 'your chat partner'} were matched based on your skills! You can now discuss your skill exchange and schedule a skill sharing session. -

-
-
+ + + ); }