diff --git a/src/app/messages/components/ChatWindow.tsx b/src/app/messages/components/ChatWindow.tsx index e92b6d2..0d35efe 100644 --- a/src/app/messages/components/ChatWindow.tsx +++ b/src/app/messages/components/ChatWindow.tsx @@ -1,5 +1,5 @@ "use client" -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { setMessages, setActiveConversation, markMessagesAsRead } from '@/store/slices/chatSlice'; import { messagingAPI } from '@/lib/api/messaging.api'; @@ -16,7 +16,7 @@ export const ChatWindow: React.FC = ({ conversationId }) => { const dispatch = useAppDispatch(); const scrollRef = useRef(null); - const { messages, conversations, activeConversationId } = useAppSelector((state) => state.chat); + const { messages, conversations, latestIncomingMessage, activeConversationId } = useAppSelector((state) => state.chat); const { user: currentUser } = useAppSelector((state) => state.auth); // `/messages` sets active chat via Redux only; `/messages/[conversationId]` passes the id prop. diff --git a/src/components/layout/Dashboard/Dashboard.tsx b/src/components/layout/Dashboard/Dashboard.tsx index 09d6b50..020fffc 100644 --- a/src/components/layout/Dashboard/Dashboard.tsx +++ b/src/components/layout/Dashboard/Dashboard.tsx @@ -77,6 +77,11 @@ export default function DashboardLayout({ children }: { children: React.ReactNod const initAuthAndFetch = async () => { // Hydrate Redux Token from LocalStorage for apiClient const savedToken = localStorage.getItem('accessToken'); + + if (!savedToken && !token) { + router.push("/auth/signin"); + return; + } if (savedToken && !token) { dispatch(setToken(savedToken)); } diff --git a/src/components/profile/PlatformUsernameInput.tsx b/src/components/profile/PlatformUsernameInput.tsx index 7933f90..79f8e82 100644 --- a/src/components/profile/PlatformUsernameInput.tsx +++ b/src/components/profile/PlatformUsernameInput.tsx @@ -51,9 +51,9 @@ export function PlatformUsernameInput({ ); const handleBlur = () => { - if (validateOnBlur && value.trim()) { + // if (validateOnBlur && value.trim()) { setShouldValidate(true); - } + // } }; const handleChange = (newValue: string) => { @@ -139,7 +139,7 @@ export function PlatformUsernameInput({ {/* Status message */} - + {/* {shouldValidate && getStatusMessage() && ( {getStatusMessage()} )} - + */} {/* Clean Preview Card */} - + {/* {showPreview && data && ( )} - + */} ); } diff --git a/src/hooks/features/useChatSocket.ts b/src/hooks/features/useChatSocket.ts index 710f5dd..25b0c11 100644 --- a/src/hooks/features/useChatSocket.ts +++ b/src/hooks/features/useChatSocket.ts @@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { messagingSocketService } from '@/lib/socket/messagingSocket'; import { addMessage, + setLatestIncomingMessage, setTypingStatus, markMessagesAsRead, deleteMessageLocal, @@ -72,6 +73,14 @@ export const useChatSocket = () => { dispatch(addMessage(message)); const isFromMe = message.sender._id === currentUserId; + if (!isFromMe) { + dispatch(setLatestIncomingMessage({ + conversationId, + messageId: message._id, + createdAt: message.createdAt, + })); + } + const isInActiveChat = conversationId === activeId; if (!isFromMe && !isInActiveChat && currentUserId) { diff --git a/src/hooks/features/usePlatformData.ts b/src/hooks/features/usePlatformData.ts index 9f08b14..9139d62 100644 --- a/src/hooks/features/usePlatformData.ts +++ b/src/hooks/features/usePlatformData.ts @@ -2,319 +2,380 @@ * Custom Hook for fetching and managing platform data */ -import { useState, useEffect, useCallback } from 'react'; -import type { PlatformType, UnifiedPlatformData, PlatformAPIError } from '@/types/platform.types'; -import { getAccessToken } from '@/store/authSelectors'; +import { useState, useEffect, useCallback } from "react"; +import type { + PlatformType, + UnifiedPlatformData, + PlatformAPIError, +} from "@/types/platform.types"; +import { getAccessToken } from "@/store/authSelectors"; interface UsePlatformDataOptions { - enabled?: boolean; - refetchInterval?: number; + enabled?: boolean; + refetchInterval?: number; } interface UsePlatformDataResult { - data: UnifiedPlatformData | null; - loading: boolean; - error: PlatformAPIError | null; - refetch: () => Promise; + data: UnifiedPlatformData | null; + loading: boolean; + error: PlatformAPIError | null; + refetch: () => Promise; } /** * Hook to fetch single platform data */ export function usePlatformData( - platform: PlatformType | null, - username: string | null, - options: UsePlatformDataOptions = {} + platform: PlatformType | null, + username: string | null, + options: UsePlatformDataOptions = {}, ): UsePlatformDataResult { - const { enabled = true, refetchInterval } = options; + const { enabled = true, refetchInterval } = options; - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); - const fetchData = useCallback(async () => { - if (!platform || !username || !enabled) { - console.log('⏭️ Skipping fetch:', { platform, username, enabled }); - return; - } - - console.log(`🚀 Fetching ${platform} data for @${username}`); - setLoading(true); - setError(null); - - try { - const url = `/api/platform/${platform}?username=${encodeURIComponent(username)}`; - console.log(`📡 API URL: ${url}`); + const fetchData = useCallback(async () => { + if (!platform || !username || !enabled) { + console.log("⏭️ Skipping fetch:", { platform, username, enabled }); + return; + } - // Get auth token from localStorage for client-side requests - const token = getAccessToken(); + console.log(`🚀 Fetching ${platform} data for @${username}`); + setLoading(true); + setError(null); - const headers: Record = { - 'Content-Type': 'application/json', - }; - - // Add auth token if available - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - const response = await fetch(url, { - headers, - }); + try { + // const url = `/api/platform/${platform}?username=${encodeURIComponent(username)}`; + // console.log(`📡 API URL: ${url}`); - // Check if response is actually JSON before parsing - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - const text = await response.text(); - console.error(`❌ Non-JSON response for ${platform}:`, text); - throw new Error(`Invalid response format: ${text.substring(0, 100)}`); - } + // Get auth token from localStorage for client-side requests + // const token = getAccessToken(); - const result = await response.json(); + // const headers: Record = { + // 'Content-Type': 'application/json', + // }; - console.log(`📥 Response for ${platform}:`, { - status: response.status, - ok: response.ok, - success: result.success, - hasData: !!result.data, - error: result.error - }); - - if (!response.ok) { - // If response is not ok, it might be a Next.js route error - if (response.status === 404) { - throw new Error(`Route not found: ${url}. Please restart the dev server if you just added this route.`); - } - throw new Error(result.error || result.message || `HTTP ${response.status}: Failed to fetch platform data`); - } - - if (!result.success) { - throw new Error(result.error || result.message || 'Failed to fetch platform data'); - } - - console.log(`✅ Successfully fetched ${platform} data:`, result.data); - setData(result.data); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - - // Handle specific error types - if (errorMessage.includes('Backend API endpoint not found') || errorMessage.includes('endpoint not found')) { - // Backend endpoint not deployed yet - log as warning, not error - console.warn(`⚠️ ${platform}: Backend API endpoint not yet available. Using stored profile data as fallback.`); - setError({ - platform, - error: 'Platform integration pending. Using stored profile data.', - type: 'endpoint_not_available' - }); - } else if (errorMessage.includes('quota') || errorMessage.includes('exceeded')) { - console.warn(`⚠️ ${platform} API quota exceeded. Consider upgrading plan or using cached data.`); - setError({ - platform, - error: 'API quota exceeded. Please try again later or upgrade your plan.', - type: 'quota_error' - }); - } else { - console.error(`❌ Error fetching ${platform} data:`, errorMessage); - setError({ - platform, - error: errorMessage, - type: 'api_error' - }); - } - setData(null); - } finally { - setLoading(false); - } - }, [platform, username, enabled]); - - useEffect(() => { - fetchData(); - - // Set up interval if specified - if (refetchInterval && refetchInterval > 0) { - const intervalId = setInterval(fetchData, refetchInterval); - return () => clearInterval(intervalId); + // Add auth token if available + // if (token) { + // headers['Authorization'] = `Bearer ${token}`; + // } + + // const response = await fetch(url, { + // headers, + // }); + + // Check if response is actually JSON before parsing + // const contentType = response.headers.get('content-type'); + // if (!contentType || !contentType.includes('application/json')) { + // const text = await response.text(); + // console.error(`❌ Non-JSON response for ${platform}:`, text); + // throw new Error(`Invalid response format: ${text.substring(0, 100)}`); + // } + + // const result = await response.json(); + + // console.log(`📥 Response for ${platform}:`, { + // // status: response.status, + // // ok: response.ok, + // success: result.success, + // hasData: !!result.data, + // error: result.error + // }); + + // if (!response.ok) { + // // If response is not ok, it might be a Next.js route error + // if (response.status === 404) { + // throw new Error(`Route not found: ${url}. Please restart the dev server if you just added this route.`); + // } + // throw new Error(result.error || result.message || `HTTP ${response.status}: Failed to fetch platform data`); + // } + + // if (!result.success) { + // throw new Error(result.error || result.message || 'Failed to fetch platform data'); + // } + + console.log(`✅ Successfully fetched ${platform} data:`); + setData({ + platform: platform, + username: username, + displayName: username, // Placeholder - replace with actual display name from API + profileImage: "string", + bio: "", + verified: true, + followers: 0, + following: 0, + totalContent: 0, + totalEngagement: 0, + averageEngagement: 0, + lastFetched: new Date(), + }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + + // Handle specific error types + if ( + errorMessage.includes("Backend API endpoint not found") || + errorMessage.includes("endpoint not found") + ) { + // Backend endpoint not deployed yet - log as warning, not error + console.warn( + `⚠️ ${platform}: Backend API endpoint not yet available. Using stored profile data as fallback.`, + ); + setError({ + platform, + error: "Platform integration pending. Using stored profile data.", + type: "endpoint_not_available", + }); + } else if ( + errorMessage.includes("quota") || + errorMessage.includes("exceeded") + ) { + console.warn( + `⚠️ ${platform} API quota exceeded. Consider upgrading plan or using cached data.`, + ); + setError({ + platform, + error: + "API quota exceeded. Please try again later or upgrade your plan.", + type: "quota_error", + }); + } else { + console.error(`❌ Error fetching ${platform} data:`, errorMessage); + setError({ + platform, + error: errorMessage, + type: "api_error", + }); } - }, [fetchData, refetchInterval]); - - return { - data, - loading, - error, - refetch: fetchData, - }; + setData(null); + } finally { + setLoading(false); + } + }, [platform, username, enabled]); + + useEffect(() => { + fetchData(); + + // Set up interval if specified + if (refetchInterval && refetchInterval > 0) { + const intervalId = setInterval(fetchData, refetchInterval); + return () => clearInterval(intervalId); + } + }, [fetchData, refetchInterval]); + + return { + data, + loading, + error, + refetch: fetchData, + }; } /** * Hook to fetch multiple platform data */ export function useMultiplePlatformData( - platforms: Array<{ type: PlatformType; username: string }>, - options: UsePlatformDataOptions = {} + platforms: Array<{ type: PlatformType; username: string }>, + options: UsePlatformDataOptions = {}, ): { - data: Record; - loading: boolean; - errors: PlatformAPIError[]; - refetch: () => Promise; + data: Record; + loading: boolean; + errors: PlatformAPIError[]; + refetch: () => Promise; } { - const { enabled = true } = options; + const { enabled = true } = options; + + const [data, setData] = useState>( + {}, + ); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState([]); + + const fetchData = useCallback(async () => { + if (!enabled || platforms.length === 0) { + console.log( + "⏭️ Skipping multiple fetch - enabled:", + enabled, + "platforms:", + platforms.length, + ); + return; + } - const [data, setData] = useState>({}); - const [loading, setLoading] = useState(false); - const [errors, setErrors] = useState([]); + console.log("🚀 Fetching multiple platforms:", platforms); + setLoading(true); + setErrors([]); - const fetchData = useCallback(async () => { - if (!enabled || platforms.length === 0) { - console.log('⏭️ Skipping multiple fetch - enabled:', enabled, 'platforms:', platforms.length); - return; - } + const results: Record = {}; + const fetchErrors: PlatformAPIError[] = []; - console.log('🚀 Fetching multiple platforms:', platforms); - setLoading(true); - setErrors([]); + // Get auth token from localStorage for client-side requests (same as single platform hook) + const token = getAccessToken(); - const results: Record = {}; - const fetchErrors: PlatformAPIError[] = []; + const headers: Record = { + "Content-Type": "application/json", + }; - // Get auth token from localStorage for client-side requests (same as single platform hook) - const token = getAccessToken(); + // Add auth token if available + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } - const headers: Record = { - 'Content-Type': 'application/json', - }; + // Fetch all platforms in parallel + await Promise.all( + platforms.map(async ({ type, username }) => { + try { + const url = `/api/platform/${type}?username=${encodeURIComponent(username)}`; + console.log(`📡 Fetching ${type}: ${url}`); - // Add auth token if available - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } + const response = await fetch(url, { + headers, + }); + const result = await response.json(); - // Fetch all platforms in parallel - await Promise.all( - platforms.map(async ({ type, username }) => { - try { - const url = `/api/platform/${type}?username=${encodeURIComponent(username)}`; - console.log(`📡 Fetching ${type}: ${url}`); - - const response = await fetch(url, { - headers, - }); - const result = await response.json(); - - console.log(`📥 ${type} response:`, { - status: response.status, - ok: response.ok, - success: result.success, - hasData: !!result.data - }); - - if (!response.ok || !result.success) { - throw new Error(result.error || 'Failed to fetch platform data'); - } - - results[type] = result.data; - console.log(`✅ ${type} data stored successfully`); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - - // Handle specific API quota errors - if (errorMessage.includes('quota') || errorMessage.includes('exceeded')) { - console.warn(`⚠️ ${type} API quota exceeded. Consider upgrading plan or using cached data.`); - fetchErrors.push({ - platform: type, - error: 'API quota exceeded. Please try again later or upgrade your plan.', - type: 'quota_error' - }); - } else if (errorMessage.includes('Backend API endpoint not found') || errorMessage.includes('endpoint not found')) { - // Backend endpoint not deployed yet - log as warning, not error - console.warn(`⚠️ ${type}: Backend API endpoint not yet available. Using stored profile data as fallback.`); - fetchErrors.push({ - platform: type, - error: 'Platform integration pending. Using stored profile data.', - type: 'endpoint_not_available' - }); - } else { - console.error(`❌ ${type} error:`, errorMessage); - fetchErrors.push({ - platform: type, - error: errorMessage, - type: 'api_error' - }); - } - - results[type] = null; - } - }) + console.log(`📥 ${type} response:`, { + status: response.status, + ok: response.ok, + success: result.success, + hasData: !!result.data, + }); + + if (!response.ok || !result.success) { + throw new Error(result.error || "Failed to fetch platform data"); + } + + results[type] = result.data; + console.log(`✅ ${type} data stored successfully`); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Unknown error"; + + // Handle specific API quota errors + if ( + errorMessage.includes("quota") || + errorMessage.includes("exceeded") + ) { + console.warn( + `⚠️ ${type} API quota exceeded. Consider upgrading plan or using cached data.`, + ); + fetchErrors.push({ + platform: type, + error: + "API quota exceeded. Please try again later or upgrade your plan.", + type: "quota_error", + }); + } else if ( + errorMessage.includes("Backend API endpoint not found") || + errorMessage.includes("endpoint not found") + ) { + // Backend endpoint not deployed yet - log as warning, not error + console.warn( + `⚠️ ${type}: Backend API endpoint not yet available. Using stored profile data as fallback.`, + ); + fetchErrors.push({ + platform: type, + error: "Platform integration pending. Using stored profile data.", + type: "endpoint_not_available", + }); + } else { + console.error(`❌ ${type} error:`, errorMessage); + fetchErrors.push({ + platform: type, + error: errorMessage, + type: "api_error", + }); + } + + results[type] = null; + } + }), + ); + + console.log("📊 Final multiple platform results:", { + results, + errors: fetchErrors, + successCount: Object.values(results).filter((v) => v !== null).length, + }); + + // Log quota errors for monitoring + const quotaErrors = fetchErrors.filter( + (error) => error.type === "quota_error", + ); + if (quotaErrors.length > 0) { + console.warn( + "🚨 API Quota Issues Detected:", + quotaErrors.map((e) => e.platform), ); - - console.log('📊 Final multiple platform results:', { - results, - errors: fetchErrors, - successCount: Object.values(results).filter(v => v !== null).length - }); - - // Log quota errors for monitoring - const quotaErrors = fetchErrors.filter(error => error.type === 'quota_error'); - if (quotaErrors.length > 0) { - console.warn('🚨 API Quota Issues Detected:', quotaErrors.map(e => e.platform)); - } - - setData(results); - setErrors(fetchErrors); - setLoading(false); - }, [platforms, enabled]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { - data: data as Record, - loading, - errors, - refetch: fetchData, - }; + } + + setData(results); + setErrors(fetchErrors); + setLoading(false); + }, [platforms, enabled]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + data: data as Record, + loading, + errors, + refetch: fetchData, + }; } /** * Hook to calculate combined metrics from multiple platforms */ export function useCombinedPlatformMetrics( - platforms: Record + platforms: Record, ) { - const [metrics, setMetrics] = useState({ - totalFollowers: 0, - totalEngagement: 0, - averageEngagementRate: 0, - platformCount: 0, - }); - - useEffect(() => { - const validPlatforms = Object.values(platforms).filter( - (p): p is UnifiedPlatformData => p !== null - ); - - if (validPlatforms.length === 0) { - setMetrics({ - totalFollowers: 0, - totalEngagement: 0, - averageEngagementRate: 0, - platformCount: 0, - }); - return; - } - - const totalFollowers = validPlatforms.reduce((sum, p) => sum + p.followers, 0); - const totalEngagement = validPlatforms.reduce((sum, p) => sum + p.totalEngagement, 0); - const averageEngagementRate = - validPlatforms.reduce((sum, p) => sum + p.averageEngagement, 0) / validPlatforms.length; - + const [metrics, setMetrics] = useState({ + totalFollowers: 0, + totalEngagement: 0, + averageEngagementRate: 0, + platformCount: 0, + }); + + useEffect(() => { + const validPlatforms = Object.values(platforms).filter( + (p): p is UnifiedPlatformData => p !== null, + ); + + if (validPlatforms.length === 0) { setMetrics({ - totalFollowers, - totalEngagement, - averageEngagementRate, - platformCount: validPlatforms.length, + totalFollowers: 0, + totalEngagement: 0, + averageEngagementRate: 0, + platformCount: 0, }); - }, [platforms]); - - return metrics; + return; + } + + const totalFollowers = validPlatforms.reduce( + (sum, p) => sum + p.followers, + 0, + ); + + const totalEngagement = validPlatforms.reduce( + (sum, p) => sum + p.totalEngagement, + 0, + ); + + const averageEngagementRate = + validPlatforms.reduce((sum, p) => sum + p.averageEngagement, 0) / + validPlatforms.length; + + setMetrics({ + totalFollowers, + totalEngagement, + averageEngagementRate, + platformCount: validPlatforms.length, + }); + }, [platforms]); + + return metrics; } diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 26eacbd..14b88bf 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -12,6 +12,7 @@ const baseURL = API_BASE_URL; // ✅ 2. Create Axios instance export const apiClient = axios.create({ baseURL, + timeout: 15000, headers: { "Content-Type": "application/json", }, @@ -64,6 +65,7 @@ apiClient.interceptors.response.use( const normalizedError = { message: responseData?.message || error.message || "Unexpected error occurred", status, + code: error.code, data: error.response?.data, }; diff --git a/src/lib/socket/events.ts b/src/lib/socket/events.ts index e042f75..78f87fc 100644 --- a/src/lib/socket/events.ts +++ b/src/lib/socket/events.ts @@ -1,6 +1,7 @@ import { io } from 'socket.io-client'; -const URL = 'http://localhost:4000'; +// const URL = 'http://localhost:4000'; +const URL = 'https://app.hyperbuds.com'; export const socket = io(URL); diff --git a/src/store/slices/chatSlice.ts b/src/store/slices/chatSlice.ts index 0fde450..b1c2d08 100644 --- a/src/store/slices/chatSlice.ts +++ b/src/store/slices/chatSlice.ts @@ -7,6 +7,11 @@ interface ChatState { messages: Message[]; isLoading: boolean; typingUsers: Record; + latestIncomingMessage: { + conversationId: string; + messageId: string; + createdAt: string; + } | null; } const initialState: ChatState = { @@ -15,6 +20,7 @@ const initialState: ChatState = { messages: [], isLoading: false, typingUsers: {}, + latestIncomingMessage: null, }; const chatSlice = createSlice({ @@ -61,6 +67,17 @@ const chatSlice = createSlice({ } }, + setLatestIncomingMessage: ( + state, + action: PayloadAction<{ + conversationId: string; + messageId: string; + createdAt: string; + } | null> + ) => { + state.latestIncomingMessage = action.payload; + }, + setTypingStatus: (state, action: PayloadAction<{ conversationId: string; userId: string; @@ -164,6 +181,7 @@ export const { setActiveConversation, setMessages, addMessage, + setLatestIncomingMessage, setTypingStatus, markMessagesAsRead, deleteMessageLocal,