From 0bfe1eedd6a4ab9ebae8eec3cb56cf00b25f2000 Mon Sep 17 00:00:00 2001 From: Kashvi Garg <97344852+kashvigarg@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:58:53 +0000 Subject: [PATCH 1/2] Fixed TL GET Req --- api/handler/timeline.go | 4 +- web2/app/api/[...path]/route.ts | 11 +- web2/app/api/sse/[...path]/route.ts | 10 +- web2/components/timeline.tsx | 70 ++++-- web2/lib/use-sse.tsx | 363 +++++++++++++++++++--------- web2/next.config.mjs | 1 + 6 files changed, 314 insertions(+), 145 deletions(-) diff --git a/api/handler/timeline.go b/api/handler/timeline.go index cff6750..2796313 100644 --- a/api/handler/timeline.go +++ b/api/handler/timeline.go @@ -102,9 +102,9 @@ func (cfg *Handler) Timeline(w http.ResponseWriter, r *http.Request) { }, }) - respondWithJson(w, http.StatusOK, timeline) - + } + respondWithJson(w, http.StatusOK, timeline) } func (cfg *Handler) Broadcasttimeline(ti timeline_item) { diff --git a/web2/app/api/[...path]/route.ts b/web2/app/api/[...path]/route.ts index 4c1684d..9e1680e 100644 --- a/web2/app/api/[...path]/route.ts +++ b/web2/app/api/[...path]/route.ts @@ -93,13 +93,14 @@ export async function GET(request: NextRequest, { params }: { params: { path: st }, }); - const text = await response.text(); // Read response as text - - if (!text.trim()) { - return NextResponse.json({ error: "Empty response from API" }, { status: 502 }); + const responseText = await response.text(); // Read response as text + // console.log("problematic text") + // console.log(responseText) + if (responseText.trim() !== "") { + // return NextResponse.json({ error: "Empty response from API" }, { status: 502 }); + return NextResponse.json(JSON.parse(responseText), { status: response.status }); } - 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 }); diff --git a/web2/app/api/sse/[...path]/route.ts b/web2/app/api/sse/[...path]/route.ts index 8e61a83..a64dcb0 100644 --- a/web2/app/api/sse/[...path]/route.ts +++ b/web2/app/api/sse/[...path]/route.ts @@ -21,7 +21,13 @@ export async function GET(request: NextRequest) { ); // Proxy the backend SSE stream directly to the client - return new Response(backendResponse.body, { + console.log(backendResponse.body) + let passedValue = await new Response(backendResponse.body).text(); + if (passedValue){ + let valueToJson = JSON.parse(passedValue); + console.log("jsonval:", valueToJson) + } + return new Response("", { status: 200, headers: { "Content-Type": "text/event-stream", @@ -32,7 +38,7 @@ export async function GET(request: NextRequest) { }, }); } catch (error) { - console.error("SSE proxy error:", error); + console.log("SSE proxy error:", error); return new Response("Failed to connect to SSE", { status: 502 }); } } diff --git a/web2/components/timeline.tsx b/web2/components/timeline.tsx index 20fb235..74c7231 100644 --- a/web2/components/timeline.tsx +++ b/web2/components/timeline.tsx @@ -34,35 +34,63 @@ export function Timeline() { const { toast } = useToast(); // Use SSE to get timeline updates - const { data, error: sseError } = useSSE("/api/sse/timeline", token, "timeline",{ - onMessage: (data) => { - if (data) { - console.log("SSE CHECK FOR TL") - console.log(data) - if (data!=null){ - setTimelineItems(data); - setIsLoading(false); + const { data, error: sseError } = useSSE("/api/sse/timeline", token, "timeline",{ + onMessage: (ndata) => { + if (ndata) { + // console.log("SSE CHECK FOR TL") + // console.log(data) + // if (data!=null){ + // setTimelineItems(data); + // setIsLoading(false); + if (ndata) { + console.log("SSE received new timeline item:", ndata); + + // Add new item to the timeline without duplicates + setTimelineItems(prevItems => { + // Check if this item already exists in our timeline + const exists = prevItems.some(item => item.id === ndata.id); + if (exists) { + return prevItems; + } + + // Add new item to the beginning of the timeline + return [ndata, ...prevItems]; + }); + + setIsLoading(false); } } - console.log("SSE NO DATA FOR TL") + console.log("sse data maybe") + console.log(data) + // console.log("SSE NO DATA FOR TL") }, fallbackToFetch: true, + onError: (err) => { + console.error("SSE error:", err); + // Let the useEffect handle errors + } }); // Fetch timeline if SSE fails useEffect(() => { - if (data) { - if (data!=null){ - setTimelineItems(data)} else { - setTimelineItems([]) - } - setIsLoading(false) - } - + fetchTimelineItems(); + + // Handle SSE errors by falling back to regular fetch if (sseError) { - fetchTimelineItems() - } - }, [data, sseError]); + console.log("SSE error detected, falling back to regular fetch:", sseError); + fetchTimelineItems();} + // if (data) { + // if (data!=null){ + // setTimelineItems(data)} else { + // setTimelineItems([]) + // } + // setIsLoading(false) + // } + + // if (sseError) { + // fetchTimelineItems() + // } + }, [sseError]); // Fetch timeline function const fetchTimelineItems = async () => { @@ -82,7 +110,7 @@ export function Timeline() { } const responseData = await response.json(); - console.log("DATA:", responseData); + // console.log("DATA:", responseData); if (responseData!=null){ setTimelineItems(responseData); diff --git a/web2/lib/use-sse.tsx b/web2/lib/use-sse.tsx index bba54d0..e19c401 100644 --- a/web2/lib/use-sse.tsx +++ b/web2/lib/use-sse.tsx @@ -1,146 +1,279 @@ -"use client" +// "use client" -import { useState, useEffect } from "react" +// import { useState, useEffect } from "react" -type SSEOptions = { - onMessage?: (data: any) => void - addEventListener?: (data: any) => void - onError?: (error: any) => void - fallbackToFetch?: boolean +// type SSEOptions = { +// onMessage?: (data: any) => void +// addEventListener?: (data: any) => void +// onError?: (error: any) => void +// fallbackToFetch?: boolean +// } + +// export function useSSE(url: string, token: string | null, eventListener: string, options: SSEOptions = {}) { +// const [data, setData] = useState(null) +// const [error, setError] = useState(null) +// const [isConnected, setIsConnected] = useState(false) + +// useEffect(() => { +// if (!token) return + +// let eventSource: EventSource | null = null +// let abortController: AbortController | null = null + +// const connectSSE = () => { +// try { +// // Try to use SSE +// console.log("Trying SSE") +// console.log(url) +// eventSource = new EventSource(`${url}?token=${token}`) + +// eventSource.onopen = () => { +// console.log("connected") +// setIsConnected(true) +// } + +// // eventSource.onmessage = (event) => { +// // console.log("TEST") +// // console.log(event.data) +// // 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.onmessage = (event) => { +// console.log("DEFAULT SSE"); +// console.log(event.data); + +// try { +// const parsedData = JSON.parse(event.data); +// setData(parsedData); +// options.onMessage?.(parsedData); +// } catch (err) { +// console.error("Error parsing SSE data", err); +// } +// }; + +// // eventSource.addEventListener(eventListener, (event) => { +// // console.log("ADDING LISTENER ", eventListener) +// // try { +// // const parsedData = JSON.parse(event.data); +// // setData(parsedData); +// // options.onMessage?.(parsedData); +// // } catch (err) { +// // console.error("Error parsing SSE data", err); +// // } +// // }); + + +// eventSource.onerror = (err) => { +// console.log("ERRRRRRRR: ", url) +// console.error("SSE error", err) +// setError(new Error("SSE connection failed")) +// options.onError?.(err) + +// // Close the connection +// eventSource?.close() + +// // If fallback is enabled, try regular fetch +// if (options.fallbackToFetch) { +// fallbackToFetch() +// } +// } +// } catch (err) { +// console.error("Failed to connect to SSE", err) +// setError(err instanceof Error ? err : new Error("Failed to connect to SSE")) + +// // If fallback is enabled, try regular fetch +// if (options.fallbackToFetch) { +// fallbackToFetch() +// } +// } +// } + +// const fallbackToFetch = async () => { +// try { +// abortController = new AbortController() + +// const response = await fetch(url, { +// headers: { +// Authorization: `Bearer ${token}`, +// }, +// signal: abortController.signal, +// }) + +// if (!response.ok) { +// throw new Error("Failed to fetch data") +// } + +// const fetchedText = await response.text() +// if (fetchedText.trim()) { +// const fetchedData = JSON.parse(fetchedText); +// setData(fetchedData) +// options.onMessage?.(fetchedData) +// } +// } catch (err) { +// if (err instanceof Error && err.name !== "AbortError") { +// console.error("Fallback fetch error", err) +// setError(err) +// options.onError?.(err) +// } +// } +// } + +// connectSSE() + +// return () => { +// if (eventSource) { +// eventSource.close() +// } + +// if (abortController) { +// abortController.abort() +// } +// } +// }, [url, token]) + +// return { data, error, isConnected } +// } + +// lib/use-sse.tsx +import { useState, useEffect, useRef } from 'react'; + +interface SSEOptions { + onMessage?: (data: T) => void; + fallbackToFetch?: boolean; + onError?: (error: Error) => void; } -export function useSSE(url: string, token: string | null, eventListener: string, options: SSEOptions = {}) { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isConnected, setIsConnected] = useState(false) +export function useSSE( + url: string, + token: string | null, + eventName: string, + options: SSEOptions = {} +) { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const eventSourceRef = useRef(null); useEffect(() => { - if (!token) return - - let eventSource: EventSource | null = null - let abortController: AbortController | null = null + // Clean up previous connection + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } - const connectSSE = () => { - try { - // Try to use SSE - console.log("Trying SSE") - console.log(url) - eventSource = new EventSource(`${url}?token=${token}`) - - eventSource.onopen = () => { - console.log("connected") - setIsConnected(true) - } + if (!token) { + setError(new Error('Authentication token is required')); + return; + } - // eventSource.onmessage = (event) => { - // console.log("TEST") - // console.log(event.data) - // 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.onmessage = (event) => { - console.log("DEFAULT SSE"); - console.log(event.data); - - try { - const parsedData = JSON.parse(event.data); + try { + // Add token as query parameter for SSE connection + const fullUrl = new URL(url, window.location.origin); + fullUrl.searchParams.append('token', token); + + // Create EventSource connection + const eventSource = new EventSource(fullUrl.toString(),{ withCredentials: true }); + eventSourceRef.current = eventSource; + + // Listen for open events + eventSource.onopen = () => { + console.log(`SSE connection opened to ${url}`); + }; + + // Listen for specific event type + eventSource.addEventListener(eventName, (event) => { + try { + console.log(`Received ${eventName} event:`, event); + + if (event.data) { + console.log(event.data) + const parsedData = JSON.parse(event.data) as T; setData(parsedData); options.onMessage?.(parsedData); - } catch (err) { - console.error("Error parsing SSE data", err); - } - }; - - // eventSource.addEventListener(eventListener, (event) => { - // console.log("ADDING LISTENER ", eventListener) - // try { - // const parsedData = JSON.parse(event.data); - // setData(parsedData); - // options.onMessage?.(parsedData); - // } catch (err) { - // console.error("Error parsing SSE data", err); - // } - // }); - - - eventSource.onerror = (err) => { - console.log("ERRRRRRRR: ", url) - console.error("SSE error", err) - setError(new Error("SSE connection failed")) - options.onError?.(err) - - // Close the connection - eventSource?.close() - - // If fallback is enabled, try regular fetch - if (options.fallbackToFetch) { - fallbackToFetch() } + } catch (err) { + console.error('Error parsing SSE data:', err); + const error = err instanceof Error ? err : new Error(String(err)); + setError(error); + options.onError?.(error); } - } catch (err) { - console.error("Failed to connect to SSE", err) - setError(err instanceof Error ? err : new Error("Failed to connect to SSE")) - - // If fallback is enabled, try regular fetch + }); + + // Listen for general error events + eventSource.onerror = (event) => { + console.error('SSE connection error:', event); + const errorMessage = 'SSE connection failed or was closed'; + setError(new Error(errorMessage)); + + if (options.onError) { + options.onError(new Error(errorMessage)); + } + + // Close the connection on error + eventSource.close(); + eventSourceRef.current = null; + + // Fallback to regular fetch if specified if (options.fallbackToFetch) { - fallbackToFetch() + console.log('Falling back to regular fetch'); + fallbackFetch(); } + }; + + // Cleanup function to close the EventSource connection + return () => { + console.log('Closing SSE connection'); + eventSource.close(); + eventSourceRef.current = null; + }; + } catch (err) { + console.error('Error setting up SSE:', err); + const error = err instanceof Error ? err : new Error(String(err)); + setError(error); + options.onError?.(error); + + // Fallback to regular fetch if there was an error setting up SSE + if (options.fallbackToFetch) { + fallbackFetch(); } } - - const fallbackToFetch = async () => { + + // Fallback fetch implementation + async function fallbackFetch() { try { - abortController = new AbortController() - const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, }, - signal: abortController.signal, - }) + }); if (!response.ok) { - throw new Error("Failed to fetch data") + throw new Error(`Fallback fetch failed with status: ${response.status}`); } - const fetchedText = await response.text() - if (fetchedText.trim()) { - const fetchedData = JSON.parse(fetchedText); - setData(fetchedData) - options.onMessage?.(fetchedData) + const responseText = await response.text(); + + if (responseText){ + const responseData = JSON.parse(responseText) + setData(responseData as T); + options.onMessage?.(responseData as T); } } catch (err) { - if (err instanceof Error && err.name !== "AbortError") { - console.error("Fallback fetch error", err) - setError(err) - options.onError?.(err) - } - } - } - - connectSSE() - - return () => { - if (eventSource) { - eventSource.close() - } - - if (abortController) { - abortController.abort() + console.error('Fallback fetch error:', err); + const error = err instanceof Error ? err : new Error(String(err)); + setError(error); + options.onError?.(error); } } - }, [url, token]) + }, [url, token, eventName]); - return { data, error, isConnected } + return { data, error }; } - diff --git a/web2/next.config.mjs b/web2/next.config.mjs index 060b74a..0878f65 100644 --- a/web2/next.config.mjs +++ b/web2/next.config.mjs @@ -7,6 +7,7 @@ try { /** @type {import('next').NextConfig} */ const nextConfig = { + reactStrictMode: true, eslint: { ignoreDuringBuilds: true, }, From 3ef310e3266661029b16b07054baacf270ca31e9 Mon Sep 17 00:00:00 2001 From: Kashvi Garg <97344852+kashvigarg@users.noreply.github.com> Date: Tue, 15 Apr 2025 02:15:12 +0000 Subject: [PATCH 2/2] Fixed Toast Component --- web2/app/layout.tsx | 9 +- web2/app/login/page.tsx | 3 +- web2/app/signup/page.tsx | 2 +- web2/components/comment-form.tsx | 2 +- web2/components/comments-list.tsx | 2 +- web2/components/compose-prose-dialog.tsx | 2 +- web2/components/notifications-list.tsx | 2 +- web2/components/prose-card.tsx | 2 +- web2/components/prose-detail.tsx | 2 +- web2/components/timeline.tsx | 2 +- web2/components/ui/toaster.tsx | 68 ++-- web2/components/ui/use-toast.ts | 389 ++++++++++++----------- web2/components/user-profile.tsx | 2 +- web2/components/users-list.tsx | 2 +- web2/lib/auth-provider.tsx | 2 +- 15 files changed, 264 insertions(+), 227 deletions(-) diff --git a/web2/app/layout.tsx b/web2/app/layout.tsx index 2c0fb30..8ef4778 100644 --- a/web2/app/layout.tsx +++ b/web2/app/layout.tsx @@ -29,14 +29,16 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - - + + + {children} + ) @@ -44,4 +46,5 @@ export default function RootLayout({ -import './globals.css' \ No newline at end of file +import './globals.css' +import { ToastProvider } from "@radix-ui/react-toast" diff --git a/web2/app/login/page.tsx b/web2/app/login/page.tsx index a66716c..b864a96 100644 --- a/web2/app/login/page.tsx +++ b/web2/app/login/page.tsx @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" import { Loader2, BookOpen } from "lucide-react" export default function LoginPage() { @@ -30,6 +30,7 @@ export default function LoginPage() { router.push("/") } catch (error) { toast({ + open: true, title: "Login failed", description: "Please check your credentials and try again.", variant: "destructive", diff --git a/web2/app/signup/page.tsx b/web2/app/signup/page.tsx index bb2dcba..052fc28 100644 --- a/web2/app/signup/page.tsx +++ b/web2/app/signup/page.tsx @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" import { Loader2, BookOpen } from "lucide-react" export default function SignupPage() { diff --git a/web2/components/comment-form.tsx b/web2/components/comment-form.tsx index 254b4bd..ff437f0 100644 --- a/web2/components/comment-form.tsx +++ b/web2/components/comment-form.tsx @@ -6,7 +6,7 @@ import { useState } from "react" import { useAuth } from "@/lib/auth-hooks" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" import { Loader2 } from "lucide-react" export function CommentForm({ proseId }: { proseId: string }) { diff --git a/web2/components/comments-list.tsx b/web2/components/comments-list.tsx index 4db12a9..eab5a60 100644 --- a/web2/components/comments-list.tsx +++ b/web2/components/comments-list.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" import { Heart } from "lucide-react" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" import { useSSE } from "@/lib/use-sse" import Link from "next/link" diff --git a/web2/components/compose-prose-dialog.tsx b/web2/components/compose-prose-dialog.tsx index ba75105..fd61aa3 100644 --- a/web2/components/compose-prose-dialog.tsx +++ b/web2/components/compose-prose-dialog.tsx @@ -15,7 +15,7 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" import { Loader2 } from "lucide-react" type ComposeProseDialogProps = { diff --git a/web2/components/notifications-list.tsx b/web2/components/notifications-list.tsx index eb3dd06..5cb6036 100644 --- a/web2/components/notifications-list.tsx +++ b/web2/components/notifications-list.tsx @@ -7,7 +7,7 @@ import { useAuth } from "@/lib/auth-hooks" import { Button } from "@/components/ui/button" import { Card, CardHeader } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" import { useSSE } from "@/lib/use-sse" import { Heart, MessageCircle, UserPlus, RefreshCw } from "lucide-react" diff --git a/web2/components/prose-card.tsx b/web2/components/prose-card.tsx index ff91165..79e5b94 100644 --- a/web2/components/prose-card.tsx +++ b/web2/components/prose-card.tsx @@ -9,7 +9,7 @@ import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { Heart, MessageCircle, Share2, MoreHorizontal } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useAuth } from "@/lib/auth-hooks" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" type ProseCardProps = { prose: { diff --git a/web2/components/prose-detail.tsx b/web2/components/prose-detail.tsx index f53b178..ba2fad8 100644 --- a/web2/components/prose-detail.tsx +++ b/web2/components/prose-detail.tsx @@ -8,7 +8,7 @@ import { CommentsList } from "@/components/comments-list" import { CommentForm } from "@/components/comment-form" import { Button } from "@/components/ui/button" import { ArrowLeft } from "lucide-react" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" type ProseDetailProps = { prose: { diff --git a/web2/components/timeline.tsx b/web2/components/timeline.tsx index 74c7231..60531e3 100644 --- a/web2/components/timeline.tsx +++ b/web2/components/timeline.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { useAuth } from "@/lib/auth-hooks"; import { ProseCard } from "@/components/prose-card"; import { Skeleton } from "@/components/ui/skeleton"; -import { useToast } from "@/components/ui/use-toast"; +import { useToast } from "@/hooks/use-toast"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertCircle, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; diff --git a/web2/components/ui/toaster.tsx b/web2/components/ui/toaster.tsx index 171beb4..5529576 100644 --- a/web2/components/ui/toaster.tsx +++ b/web2/components/ui/toaster.tsx @@ -1,3 +1,40 @@ +// "use client" + +// import { useToast } from "@/hooks/use-toast" +// import { +// Toast, +// ToastClose, +// ToastDescription, +// ToastProvider, +// ToastTitle, +// ToastViewport, +// } from "@/components/ui/toast" + +// export function Toaster() { +// const { toasts } = useToast() +// console.log("TOASTER RECEIVED TOASTS:", toasts); + +// return ( +// +// {toasts.map(function ({ id, title, description, action, ...props }) { +// return ( +// +//
+// {title && {title}} +// {description && ( +// {description} +// )} +//
+// {action} +// +//
+// ) +// })} +// +//
+// ) +// } + "use client" import { useToast } from "@/hooks/use-toast" @@ -5,31 +42,26 @@ import { Toast, ToastClose, ToastDescription, - ToastProvider, ToastTitle, ToastViewport, } from "@/components/ui/toast" export function Toaster() { const { toasts } = useToast() - + return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ) - })} + <> + {toasts.map(({ id, title, description, action, ...props }) => ( + +
+ {title && {title}} + {description && {description}} +
+ {action} + +
+ ))} -
+ ) } diff --git a/web2/components/ui/use-toast.ts b/web2/components/ui/use-toast.ts index 02e111d..06031f1 100644 --- a/web2/components/ui/use-toast.ts +++ b/web2/components/ui/use-toast.ts @@ -1,194 +1,195 @@ -"use client" - -// Inspired by react-hot-toast library -import * as React from "react" - -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { useToast, toast } +// "use client" + +// // Inspired by react-hot-toast library +// import * as React from "react" + +// import type { +// ToastActionElement, +// ToastProps, +// } from "@/components/ui/toast" + +// const TOAST_LIMIT = 1 +// const TOAST_REMOVE_DELAY = 1000000 + +// type ToasterToast = ToastProps & { +// id: string +// title?: React.ReactNode +// description?: React.ReactNode +// action?: ToastActionElement +// } + +// const actionTypes = { +// ADD_TOAST: "ADD_TOAST", +// UPDATE_TOAST: "UPDATE_TOAST", +// DISMISS_TOAST: "DISMISS_TOAST", +// REMOVE_TOAST: "REMOVE_TOAST", +// } as const + +// let count = 0 + +// function genId() { +// count = (count + 1) % Number.MAX_SAFE_INTEGER +// return count.toString() +// } + +// type ActionType = typeof actionTypes + +// type Action = +// | { +// type: ActionType["ADD_TOAST"] +// toast: ToasterToast +// } +// | { +// type: ActionType["UPDATE_TOAST"] +// toast: Partial +// } +// | { +// type: ActionType["DISMISS_TOAST"] +// toastId?: ToasterToast["id"] +// } +// | { +// type: ActionType["REMOVE_TOAST"] +// toastId?: ToasterToast["id"] +// } + +// interface State { +// toasts: ToasterToast[] +// } + +// const toastTimeouts = new Map>() + +// const addToRemoveQueue = (toastId: string) => { +// if (toastTimeouts.has(toastId)) { +// return +// } + +// const timeout = setTimeout(() => { +// toastTimeouts.delete(toastId) +// dispatch({ +// type: "REMOVE_TOAST", +// toastId: toastId, +// }) +// }, TOAST_REMOVE_DELAY) + +// toastTimeouts.set(toastId, timeout) +// } + +// export const reducer = (state: State, action: Action): State => { +// switch (action.type) { +// case "ADD_TOAST": +// return { +// ...state, +// toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), +// } + +// case "UPDATE_TOAST": +// return { +// ...state, +// toasts: state.toasts.map((t) => +// t.id === action.toast.id ? { ...t, ...action.toast } : t +// ), +// } + +// case "DISMISS_TOAST": { +// const { toastId } = action + +// // ! Side effects ! - This could be extracted into a dismissToast() action, +// // but I'll keep it here for simplicity +// if (toastId) { +// addToRemoveQueue(toastId) +// } else { +// state.toasts.forEach((toast) => { +// addToRemoveQueue(toast.id) +// }) +// } + +// return { +// ...state, +// toasts: state.toasts.map((t) => +// t.id === toastId || toastId === undefined +// ? { +// ...t, +// open: false, +// } +// : t +// ), +// } +// } +// case "REMOVE_TOAST": +// if (action.toastId === undefined) { +// return { +// ...state, +// toasts: [], +// } +// } +// return { +// ...state, +// toasts: state.toasts.filter((t) => t.id !== action.toastId), +// } +// } +// } + +// const listeners: Array<(state: State) => void> = [] + +// let memoryState: State = { toasts: [] } + +// function dispatch(action: Action) { +// memoryState = reducer(memoryState, action) +// listeners.forEach((listener) => { +// listener(memoryState) +// }) +// } + +// type Toast = Omit + +// function toast({ ...props }: Toast) { +// const id = genId() +// console.log("TOAST TRIGGERED:", props); + +// const update = (props: ToasterToast) => +// dispatch({ +// type: "UPDATE_TOAST", +// toast: { ...props, id }, +// }) +// const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + +// dispatch({ +// type: "ADD_TOAST", +// toast: { +// ...props, +// id, +// open: true, +// onOpenChange: (open) => { +// if (!open) dismiss() +// }, +// }, +// }) + +// return { +// id: id, +// dismiss, +// update, +// } +// } + +// function useToast() { +// const [state, setState] = React.useState(memoryState) + +// React.useEffect(() => { +// listeners.push(setState) +// return () => { +// const index = listeners.indexOf(setState) +// if (index > -1) { +// listeners.splice(index, 1) +// } +// } +// }, [state]) + +// return { +// ...state, +// toast, +// dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), +// } +// } + +// export { useToast, toast } diff --git a/web2/components/user-profile.tsx b/web2/components/user-profile.tsx index f6affff..2304cf8 100644 --- a/web2/components/user-profile.tsx +++ b/web2/components/user-profile.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ProseCard } from "@/components/prose-card"; -import { useToast } from "@/components/ui/use-toast"; +import { useToast } from "@/hooks/use-toast"; import { Loader2 } from "lucide-react"; type Prose = { diff --git a/web2/components/users-list.tsx b/web2/components/users-list.tsx index 2242ef7..216985b 100644 --- a/web2/components/users-list.tsx +++ b/web2/components/users-list.tsx @@ -5,7 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; -import { useToast } from "@/components/ui/use-toast"; +import { useToast } from "@/hooks/use-toast"; import Link from "next/link"; type User = { diff --git a/web2/lib/auth-provider.tsx b/web2/lib/auth-provider.tsx index 52607ea..c77fad2 100644 --- a/web2/lib/auth-provider.tsx +++ b/web2/lib/auth-provider.tsx @@ -4,7 +4,7 @@ import type React from "react" import { createContext, useState, useEffect } from "react" import { useRouter } from "next/navigation" -import { useToast } from "@/components/ui/use-toast" +import { useToast } from "@/hooks/use-toast" type User = { id: string