diff --git a/web2/app/api/[...path]/route.ts b/web2/app/api/[...path]/route.ts index 24124ff..51d7c71 100644 --- a/web2/app/api/[...path]/route.ts +++ b/web2/app/api/[...path]/route.ts @@ -2,14 +2,13 @@ import { type NextRequest, NextResponse } from "next/server" // This is a proxy API route that forwards requests to the backend export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) { - // const path = params.path.join("/") const url = new URL(request.url); const path = url.pathname.replace("/api/", ""); - const { searchParams } = new URL(request.url) - const token = searchParams.get("token") || request.headers.get("Authorization")?.split(" ")[1] + const { searchParams } = new URL(request.url); + const token = searchParams.get("token") || request.headers.get("Authorization")?.split(" ")[1]; // Check if this is an SSE endpoint - const isSSE = path === "timeline" || path === "notifications" || path.includes("/comments") + const isSSE = path === "timeline" || path === "notifications" || path.includes("/comments"); if (isSSE) { try { @@ -19,45 +18,68 @@ export async function GET(request: NextRequest, { params }: { params: { path: st Authorization: `Bearer ${token}`, Accept: "text/event-stream", }, - }) - - if (response.ok) { - // Set up SSE response - const encoder = new TextEncoder() - const stream = new ReadableStream({ - async start(controller) { - const data = await response.json() - - // Send the initial data - controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)) - - // Keep the connection open - const interval = setInterval(() => { - controller.enqueue(encoder.encode(": keepalive\n\n")) - }, 30000) - - // Clean up on close - request.signal.addEventListener("abort", () => { - clearInterval(interval) - controller.close() - }) - }, - }) - - return new NextResponse(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }) + }); + + const text = await response.text(); + + if (response.ok && text.trim()) { + try { + const data = JSON.parse(text); + + // Set up SSE response + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); + + // Keep the connection open + const interval = setInterval(() => { + controller.enqueue(encoder.encode(": keepalive\n\n")); + }, 30000); + + // Clean up on close + request.signal.addEventListener("abort", () => { + clearInterval(interval); + controller.close(); + }); + }, + }); + + return new NextResponse(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); + } catch (jsonError) { + console.error("SSE JSON parse error:", jsonError); + return NextResponse.json({ error: "Invalid JSON response from SSE" }, { status: 502 }); + } } else { - // Fall back to regular response - return NextResponse.json(await response.json(), { status: response.status }) + console.log("SSE response empty, falling back to basic fetch..."); + + const fallbackResponse = await fetch(`${process.env.API_URL}/api/${path}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + const fallbackText = await fallbackResponse.text(); + + if (!fallbackText.trim()) { + return NextResponse.json({ error: "Empty response from server" }, { status: 502 }); + } + + try { + return NextResponse.json(JSON.parse(fallbackText), { status: fallbackResponse.status }); + } catch { + return NextResponse.json({ error: "Invalid JSON from fallback" }, { status: 502 }); + } + } + } catch (error: any) { + console.error("SSE error:", error); + if (error.code !== "UND_ERR_HEADERS_TIMEOUT") { + return NextResponse.json({ error: "Failed to connect to SSE" }, { status: 500 }); } - } catch (error) { - console.error("SSE error:", error) - return NextResponse.json({ error: "Failed to connect to SSE" }, { status: 500 }) } } @@ -68,13 +90,18 @@ export async function GET(request: NextRequest, { params }: { params: { path: st Authorization: token ? `Bearer ${token}` : "", "Content-Type": request.headers.get("Content-Type") || "application/json", }, - }) + }); - const data = await response.json() - return NextResponse.json(data, { status: response.status }) + const text = await response.text(); // Read response as text + + if (!text.trim()) { + return NextResponse.json({ error: "Empty response from API" }, { status: 502 }); + } + + return NextResponse.json(JSON.parse(text), { status: response.status }); } catch (error) { - console.error("API error:", error) - return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 }) + console.error("API error:", error); + return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 }); } } diff --git a/web2/app/profile/[username]/page.tsx b/web2/app/profile/[username]/page.tsx index b194de9..e618223 100644 --- a/web2/app/profile/[username]/page.tsx +++ b/web2/app/profile/[username]/page.tsx @@ -4,8 +4,12 @@ import { MainLayout } from "@/components/main-layout" import { UserProfile } from "@/components/user-profile" import { getUserProfile } from "@/lib/api" -export default async function ProfilePage(props: { params: Promise<{ username: string }> }) { - const params = await props.params; +export default async function ProfilePage({ + params, +}: { + params: Promise<{ username: string }> +}) { + const {username} = await params; const cookieStore = await cookies() const token = cookieStore.get("auth_token") @@ -13,7 +17,7 @@ export default async function ProfilePage(props: { params: Promise<{ username: s redirect("/login") } - const userData = await getUserProfile(params.username, token.value) + const userData = await getUserProfile(username, token.value) return ( diff --git a/web2/app/prose/[proseId]/page.tsx b/web2/app/prose/[proseId]/page.tsx index 04a16c8..7f1233a 100644 --- a/web2/app/prose/[proseId]/page.tsx +++ b/web2/app/prose/[proseId]/page.tsx @@ -4,8 +4,8 @@ import { MainLayout } from "@/components/main-layout" import { ProseDetail } from "@/components/prose-detail" import { getProseById } from "@/lib/api" -export default async function ProsePage(props: { params: Promise<{ proseId: string }> }) { - const params = await props.params; +export default async function ProsePage({params,}: { params: Promise<{ proseId: string }> }) { + const {proseId} = await params; const cookieStore = await cookies() const token = cookieStore.get("auth_token") @@ -13,7 +13,7 @@ export default async function ProsePage(props: { params: Promise<{ proseId: stri redirect("/login") } - const proseData = await getProseById(params.proseId, token.value) + const proseData = await getProseById(proseId, token.value) return ( diff --git a/web2/components/comments-list.tsx b/web2/components/comments-list.tsx index c0ae326..c41ff9c 100644 --- a/web2/components/comments-list.tsx +++ b/web2/components/comments-list.tsx @@ -37,6 +37,8 @@ export function CommentsList({ proseId }: { proseId: string }) { const { data, error: sseError } = useSSE(`/api/${proseId}/comments`, token, { onMessage: (data) => { if (data) { + console.log("SSE CHECK FOR COMMENTS") + console.log(data) setComments(data) setIsLoading(false) } @@ -68,6 +70,7 @@ export function CommentsList({ proseId }: { proseId: string }) { } const data = await response.json() + console.log(data) setComments(data) } catch (err) { toast({ @@ -116,7 +119,7 @@ export function CommentsList({ proseId }: { proseId: string }) { }) } } - + console.log(comments) if (isLoading) { return (
@@ -144,6 +147,7 @@ export function CommentsList({ proseId }: { proseId: string }) { ) } + return (
{comments.map((comment) => ( diff --git a/web2/components/notification-indicator.tsx b/web2/components/notification-indicator.tsx index 2ea0308..247b7fe 100644 --- a/web2/components/notification-indicator.tsx +++ b/web2/components/notification-indicator.tsx @@ -37,8 +37,15 @@ export function NotificationIndicator({ className = "" }: NotificationIndicatorP }) if (response.ok) { - const data = await response.json() + const text = await response.text() + const data = JSON.parse(text) + if (data!=null){ setHasUnread(data.some((notification: any) => !notification.read)) + } else { + setHasUnread(false) + console.log("EMPTY") + console.log(data) + } } } catch (err) { console.error("Failed to check unread notifications", err) diff --git a/web2/components/notifications-list.tsx b/web2/components/notifications-list.tsx index 2329b5d..6513588 100644 --- a/web2/components/notifications-list.tsx +++ b/web2/components/notifications-list.tsx @@ -31,7 +31,12 @@ export function NotificationsList() { const { data, error: sseError } = useSSE("/api/notifications", token, { onMessage: (data) => { if (data) { - setNotifications(data) + console.log("SSE CHECK FOR NOTIFS") + console.log(data) + if (data!=null){ + setNotifications(data)} else { + setNotifications([]) + } setIsLoading(false) } }, @@ -40,7 +45,10 @@ export function NotificationsList() { useEffect(() => { if (data) { - setNotifications(data) + if (data!=null){ + setNotifications(data)} else { + setNotifications([]) + } setIsLoading(false) } @@ -62,7 +70,10 @@ export function NotificationsList() { } const data = await response.json() - setNotifications(data) + if (data!=null){ + setNotifications(data.filter((notification: Notification) => !notification.read));} else { + setNotifications([]) + } } catch (err) { toast({ title: "Error", @@ -74,6 +85,10 @@ export function NotificationsList() { } } + const removeNotification = (notificationId: string) => { + setNotifications((prev) => prev.filter((n) => n.id !== notificationId)); + }; + const markAllAsRead = async () => { try { const response = await fetch("/api/notifications/mark_as_read", { @@ -81,25 +96,33 @@ export function NotificationsList() { headers: { Authorization: `Bearer ${token}`, }, + body: JSON.stringify("") }) + console.log(response) + if (!response.ok) { throw new Error("Failed to mark notifications as read") } // Update all notifications as read - setNotifications( - notifications.map((notification) => ({ - ...notification, - read: true, - })), - ) + // if (notifications!=null){ + // setNotifications( + // notifications.map((notification) => ({ + // ...notification, + // read: true, + // })), + // )} else { + setNotifications([]) + // } + toast({ title: "Notifications marked as read", description: "All notifications have been marked as read", }) } catch (err) { + console.log(err) toast({ title: "Error", description: "Failed to mark notifications as read", @@ -115,24 +138,15 @@ export function NotificationsList() { headers: { Authorization: `Bearer ${token}`, }, + body: JSON.stringify("") }) - + console.log(response) if (!response.ok) { throw new Error("Failed to mark notification as read") } // Update the notification as read - setNotifications( - notifications.map((notification) => { - if (notification.id === notificationId) { - return { - ...notification, - read: true, - } - } - return notification - }), - ) + removeNotification(notificationId) } catch (err) { toast({ title: "Error", @@ -173,7 +187,7 @@ export function NotificationsList() { return ( <> {actorText} - {" commented on your verse"} + {" commented on your prose"} ) case "follow": @@ -215,18 +229,21 @@ export function NotificationsList() { Refresh + { notifications != null? ( + ) : (null)}
-
+ - {notifications.length === 0 ? ( + {notifications!= null && notifications.length === 0 ? (

No notifications yet.

) : ( notifications.map((notification) => ( + ([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { toast } = useToast(); - // Try to use SSE for timeline, fall back to regular fetch + // Use SSE to get timeline updates const { data, error: sseError } = useSSE("/api/timeline", token, { onMessage: (data) => { if (data) { + console.log("SSE CHECK FOR TL") + console.log(data) + if (data!=null){ setTimelineItems(data); setIsLoading(false); + } } + console.log("SSE NO DATA FOR TL") }, fallbackToFetch: true, }); - useEffect(() => { - refreshTimeline(); // Ensure data is fetched when the page loads - }, []); - + // Fetch timeline if SSE fails useEffect(() => { if (data) { - setTimelineItems(data); - setIsLoading(false); + if (data!=null){ + setTimelineItems(data)} else { + setTimelineItems([]) + } + setIsLoading(false) } if (sseError) { - setError("Failed to load timeline. Please try again."); - setIsLoading(false); + fetchTimelineItems() } }, [data, sseError]); - const refreshTimeline = async () => { + // Fetch timeline function + const fetchTimelineItems = async () => { console.log("TIMELINE CALL"); setIsLoading(true); setError(null); @@ -73,17 +76,19 @@ export function Timeline() { Authorization: `Bearer ${token}`, }, }); - console.log("RESPONSE") - console.log(response) if (!response.ok) { throw new Error("Failed to refresh timeline"); } - const data = await response.json(); - console.log("DATA") - console.log(data); - setTimelineItems(data); + const responseData = await response.json(); + console.log("DATA:", responseData); + + if (responseData!=null){ + setTimelineItems(responseData); + } else { + setTimelineItems([]) + } } catch (err) { setError("Failed to refresh timeline. Please try again."); toast({ @@ -96,6 +101,7 @@ export function Timeline() { } }; + // Handle like toggles const handleLikeToggle = async (proseId: string) => { try { const response = await fetch(`/api/prose/${proseId}/togglelike`, { @@ -111,7 +117,7 @@ export function Timeline() { const result = await response.json(); - // Update the prose in the list + // Update the liked state in the timeline setTimelineItems( timelineItems.map((item) => { if (item.prose.id === proseId) { @@ -140,7 +146,7 @@ export function Timeline() {

Your Timeline

- @@ -169,7 +175,7 @@ export function Timeline() { {error} - diff --git a/web2/components/user-profile.tsx b/web2/components/user-profile.tsx index 83f8b3e..f6affff 100644 --- a/web2/components/user-profile.tsx +++ b/web2/components/user-profile.tsx @@ -37,8 +37,8 @@ type UserProfileProps = { export function UserProfile({ user: initialUser }: UserProfileProps) { const [user, setUser] = useState(initialUser); - // const [proses, setProses] = useState([]); - const proses: Array = [] + const [proses, setProses] = useState([]); + // const proses: Array = [] const [isLoading, setIsLoading] = useState(false); const { user: currentUser, token } = useAuth(); const [error, setError] = useState(null); @@ -64,7 +64,7 @@ export function UserProfile({ user: initialUser }: UserProfileProps) { } const data = await response.json(); - // setProses(data); + setProses(data.map((p: Prose) => ({ ...p, username: p.username || user.username }))); } catch (err) { setError("Failed to load prose. Please try again."); toast({ @@ -122,6 +122,7 @@ export function UserProfile({ user: initialUser }: UserProfileProps) { headers: { Authorization: `Bearer ${token}`, }, + body: JSON.stringify("") }); if (!response.ok) { @@ -130,9 +131,9 @@ export function UserProfile({ user: initialUser }: UserProfileProps) { const result = await response.json(); - // setProses(proses.map(prose => - // prose.id === proseId ? { ...prose, liked: result.liked, likes_count: result.likes_count } : prose - // )); + setProses(proses.map(prose => + prose.id === proseId ? { ...prose, liked: result.liked, likes_count: result.likes_count } : prose + )); } catch (err) { toast({ title: "Error", @@ -157,7 +158,7 @@ export function UserProfile({ user: initialUser }: UserProfileProps) { throw new Error("Failed to delete verse"); } - // setProses(proses.filter(prose => prose.id !== proseId)); + setProses(proses.filter(prose => prose.id !== proseId)); toast({ title: "Verse deleted", @@ -214,7 +215,7 @@ export function UserProfile({ user: initialUser }: UserProfileProps) { Verses - Likes + {/* Likes */} diff --git a/web2/hooks/use-toast.ts b/web2/hooks/use-toast.ts index 02e111d..0fdf258 100644 --- a/web2/hooks/use-toast.ts +++ b/web2/hooks/use-toast.ts @@ -182,7 +182,7 @@ function useToast() { listeners.splice(index, 1) } } - }, [state]) + }, []) return { ...state, diff --git a/web2/lib/auth-provider.tsx b/web2/lib/auth-provider.tsx index 3121524..52607ea 100644 --- a/web2/lib/auth-provider.tsx +++ b/web2/lib/auth-provider.tsx @@ -163,6 +163,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }) if (!response.ok) { + console.log(response) throw new Error("Signup failed") } @@ -213,17 +214,32 @@ function setCookie(name: string, value: string, days: number) { document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/` } -function getCookie(name: string) { - const nameEQ = name + "=" - const ca = document.cookie.split(";") - for (let i = 0; i < ca.length; i++) { - let c = ca[i] - while (c.charAt(0) === " ") c = c.substring(1, c.length) - if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length) +// function getCookie(name: string) { +// const nameEQ = name + "=" +// const ca = document.cookie.split(";") +// for (let i = 0; i < ca.length; i++) { +// let c = ca[i] +// while (c.charAt(0) === " ") c = c.substring(1, c.length) +// if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length) +// } +// return null +// } + +function getCookie(name: string): string | null { + const nameEQ = name + "="; + const cookies = document.cookie.split(";"); + + for (const cookie of cookies) { + const trimmedCookie = cookie.trim(); + if (trimmedCookie.startsWith(nameEQ)) { + return trimmedCookie.substring(nameEQ.length); + } } - return null + + return null; } + function deleteCookie(name: string) { document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/` } diff --git a/web2/lib/use-sse.tsx b/web2/lib/use-sse.tsx index 3c9b852..35554f6 100644 --- a/web2/lib/use-sse.tsx +++ b/web2/lib/use-sse.tsx @@ -22,21 +22,25 @@ export function useSSE(url: string, token: string | null, options: SSEOptions const connectSSE = () => { try { // Try to use SSE - eventSource = new EventSource(`${url}?token=${token}`) + eventSource = new EventSource(`${url}?token=${token}&sse=true`) eventSource.onopen = () => { setIsConnected(true) } eventSource.onmessage = (event) => { - try { - const parsedData = JSON.parse(event.data) - setData(parsedData) - options.onMessage?.(parsedData) - } catch (err) { - console.error("Error parsing SSE data", err) + if (event.data.startsWith("data:")) { + try { + const parsedData = JSON.parse(event.data.replace(/^data: /, "")); + setData(parsedData); + options.onMessage?.(parsedData); + } catch (err) { + console.error("Error parsing SSE data", err); + } + } else { + console.warn("Received non-SSE data", event.data); } - } + }; eventSource.onerror = (err) => { console.error("SSE error", err)