diff --git a/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx b/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx index 0ae124f3..6905aaca 100644 --- a/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx @@ -18,12 +18,12 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import React, { useRef, useState, useEffect } from "react"; import { Camera, Loader2, Plus } from "lucide-react"; -import { toast } from "@/hooks/use-toast"; import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import { getStaticFileUrl, isDarkColor } from "../../utils/others"; import { defaultFooterProperties, useFooterContext } from "../../context/footerContext"; import { FooterProperties } from "../../services/footerService"; +import { toast } from "sonner"; const SlideFooter: React.FC = () => { const [showEditor, setShowEditor] = useState(false); @@ -43,17 +43,13 @@ const SlideFooter: React.FC = () => { const handleSave = async () => { await saveFooterProperties(footerProperties); setIsPropertyChanged(false); - toast({ - title: "Footer properties saved successfully", - }); + toast.success("Footer properties saved successfully"); }; const handleReset = async () => { await resetFooterProperties(); setFooterProperties(defaultFooterProperties); - toast({ - title: "Footer properties reset to default", - }); + toast.success("Footer properties reset to default"); }; const updateProperty = (path: string, value: any): void => { @@ -186,9 +182,7 @@ const SlideFooter: React.FC = () => { if (!file) return; if (!file.type.startsWith("image/")) { - toast({ - title: "Please Upload An Image File", - }); + toast.error("Please Upload An Image File"); return; } @@ -208,10 +202,7 @@ const SlideFooter: React.FC = () => { })); } catch (error) { console.error("Error converting image:", error); - toast({ - title: "Error uploading image", - variant: "destructive", - }); + toast.error("Error uploading image"); } finally { setIsUploading({ ...isUploading, white: false }); } @@ -225,9 +216,7 @@ const SlideFooter: React.FC = () => { if (!file) return; if (!file.type.startsWith("image/")) { - toast({ - title: "Please Upload An Image File", - }); + toast.error("Please Upload An Image File"); return; } @@ -247,10 +236,7 @@ const SlideFooter: React.FC = () => { })); } catch (error) { console.error("Error converting image:", error); - toast({ - title: "Error uploading image", - variant: "destructive", - }); + toast.error("Error uploading image"); } finally { setIsUploading({ ...isUploading, dark: false }); } @@ -277,10 +263,8 @@ const SlideFooter: React.FC = () => { const handleSheetClose = () => { if (isPropertyChanged) { - toast({ - title: "Unsaved Changes", + toast.error("Unsaved Changes", { description: "Please save changes before closing the editor", - variant: "destructive", }); return; } diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index 77550392..20f2c292 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -1,7 +1,7 @@ "use client"; import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import dynamic from 'next/dynamic'; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import * as z from 'zod'; export interface LayoutInfo { @@ -98,8 +98,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`); if (!module.default) { - toast({ - title: `${file} has no default export`, + toast.error(`${file} has no default export`, { description: 'Please ensure the layout file exports a default component', }); console.warn(`❌ ${file} has no default export`); @@ -107,8 +106,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) } if (!module.Schema) { - toast({ - title: `${file} has no Schema export`, + toast.error(`${file} has no Schema export`, { description: 'Please ensure the layout file exports a Schema', }); console.warn(`❌ ${file} has no Schema export`); diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index 8357ea4f..7b106d23 100644 --- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -22,7 +22,7 @@ import { useDispatch, useSelector } from "react-redux"; import { useRouter } from "next/navigation"; import { RootState } from "@/store/store"; import { Button } from "@/components/ui/button"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import MarkdownRenderer from "./MarkdownRenderer"; import { getIconFromFile } from "../../utils/others"; import { ChevronRight, PanelRightOpen, X } from "lucide-react"; @@ -122,11 +122,7 @@ const DocumentsPreviewPage: React.FC = () => { }); } catch (error) { console.error('Error reading files:', error); - toast({ - title: "Error", - description: "Failed to read document content", - variant: "destructive", - }); + toast.error("Failed to read document content"); } setDownloadingDocuments([]); } @@ -155,11 +151,7 @@ const DocumentsPreviewPage: React.FC = () => { router.push("/outline"); } catch (error) { console.error("Error in presentation creation:", error); - toast({ - title: "Error in presentation creation.", - description: "Please try again.", - variant: "destructive", - }); + toast.error("Error in presentation creation. Please try again."); setShowLoading({ message: "Error in presentation creation.", show: true, diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts index bd84a275..24a81604 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration"; import { jsonrepair } from "jsonrepair"; import { StreamState } from "../types/index"; @@ -56,11 +56,7 @@ export const useOutlineStreaming = (presentationId: string | null) => { eventSource.close(); } catch (error) { console.error("Error parsing accumulated chunks:", error); - toast({ - title: "Error", - description: "Failed to parse presentation data", - variant: "destructive", - }); + toast.error("Failed to parse presentation data"); eventSource.close(); } accumulatedChunks = ""; @@ -76,19 +72,11 @@ export const useOutlineStreaming = (presentationId: string | null) => { eventSource.onerror = () => { setStreamState({ isStreaming: false, isLoading: false }); eventSource.close(); - toast({ - title: "Connection Error", - description: "Failed to connect to the server. Please try again.", - variant: "destructive", - }); + toast.error("Failed to connect to the server. Please try again."); }; } catch (error) { setStreamState({ isStreaming: false, isLoading: false }); - toast({ - title: "Error", - description: "Failed to initialize connection", - variant: "destructive", - }); + toast.error("Failed to initialize connection"); } }; diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts index 79a492ed..b8ee619e 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts @@ -1,7 +1,7 @@ import { useState, useCallback } from "react"; import { useDispatch } from "react-redux"; import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { clearPresentationData, setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import { useLayout } from "../../context/LayoutContext"; @@ -27,19 +27,15 @@ export const usePresentationGeneration = ( const validateInputs = useCallback(() => { if (!outlines || outlines.length === 0) { - toast({ - title: "No Outlines", + toast.error("No Outlines", { description: "Please wait for outlines to load before generating presentation", - variant: "destructive", }); return false; } if (!selectedLayoutGroup) { - toast({ - title: "Select Layout Group", + toast.error("Select Layout Group", { description: "Please select a layout group before generating presentation", - variant: "destructive", }); return false; } @@ -101,10 +97,8 @@ export const usePresentationGeneration = ( } } catch (error) { console.error("Error in data generation", error); - toast({ - title: "Generation Error", + toast.error("Generation Error", { description: "Failed to generate presentation. Please try again.", - variant: "destructive", }); } finally { setLoadingState(DEFAULT_LOADING_STATE); diff --git a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx index e1cb7b4d..9c63e419 100644 --- a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx @@ -8,7 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { DashboardApi } from "@/app/dashboard/api/dashboard"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; @@ -40,11 +40,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { setContentLoading(false); } catch (error) { setError(true); - toast({ - title: "Error", - description: "Failed to load presentation", - variant: "destructive", - }); + toast.error("Failed to load presentation"); console.error("Error fetching user slides:", error); setContentLoading(false); } diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx index 64d3a9b7..c198445b 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx @@ -20,7 +20,7 @@ import { useDispatch, useSelector } from "react-redux"; import Link from "next/link"; import { RootState } from "@/store/store"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import Modal from "./Modal"; @@ -75,11 +75,9 @@ const Header = ({ } catch (error) { console.error("Export failed:", error); setShowLoader(false); - toast({ - title: "Having trouble exporting!", + toast.error("Having trouble exporting!", { description: "We are having trouble exporting your presentation. Please try again.", - variant: "default", }); } finally { setShowLoader(false); @@ -111,11 +109,9 @@ const Header = ({ } catch (err) { console.error(err); - toast({ - title: "Having trouble exporting!", + toast.error("Having trouble exporting!", { description: "We are having trouble exporting your presentation. Please try again.", - variant: "default", }); } finally { setShowLoader(false); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index a260734a..beda2448 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -8,7 +8,7 @@ import { } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; import { SendHorizontal } from "lucide-react"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import ToolTip from "@/components/ToolTip"; import { RootState } from "@/store/store"; @@ -45,11 +45,7 @@ const SlideContent = ({ ) as HTMLInputElement; const value = element?.value; if (!value?.trim()) { - toast({ - title: "Error", - description: "Please enter a prompt before submitting", - variant: "destructive", - }); + toast.error("Please enter a prompt before submitting"); return; } setIsUpdating(true); @@ -64,18 +60,11 @@ const SlideContent = ({ if (response) { console.log("response", response); dispatch(updateSlide({ index: slide.index, slide: response })); - toast({ - title: "Success", - description: "Slide updated successfully", - }); + toast.success("Slide updated successfully"); } } catch (error) { console.error("Error updating slide:", error); - toast({ - title: "Error", - description: "Failed to update slide. Please try again.", - variant: "destructive", - }); + toast.error("Failed to update slide. Please try again."); } finally { setIsUpdating(false); } diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx b/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx index 596e2405..963b17cc 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx @@ -13,7 +13,7 @@ export const useAutoSave = ({ debounceMs = 2000, enabled = true, }: UseAutoSaveOptions = {}) => { - const { presentationData } = useSelector( + const { presentationData, isStreaming, isLoading } = useSelector( (state: RootState) => state.presentationGeneration ); @@ -61,7 +61,7 @@ export const useAutoSave = ({ // Effect to trigger auto-save when presentation data changes useEffect(() => { - if (!enabled || !presentationData) return; + if (!enabled || !presentationData || isStreaming || isLoading) return; // Trigger debounced save debouncedSave(presentationData); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts index 0369c342..90d60c2c 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts @@ -1,6 +1,6 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from 'react'; import { useDispatch } from "react-redux"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { DashboardApi } from "@/app/dashboard/api/dashboard"; import { setPresentationData } from "@/store/slices/presentationGeneration"; @@ -11,7 +11,6 @@ export const usePresentationData = ( ) => { const dispatch = useDispatch(); - const fetchUserSlides = useCallback(async () => { try { const data = await DashboardApi.getPresentation(presentationId); @@ -21,20 +20,17 @@ export const usePresentationData = ( } } catch (error) { setError(true); - toast({ - title: "Error", - description: "Failed to load presentation", - variant: "destructive", - }); + toast.error("Failed to load presentation"); console.error("Error fetching user slides:", error); setLoading(false); } }, [presentationId, dispatch, setLoading, setError]); - + useEffect(() => { + fetchUserSlides(); + }, [fetchUserSlides]); return { fetchUserSlides, - }; }; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts index 66b7c1b1..78ccea88 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts @@ -1,7 +1,8 @@ import { useEffect, useRef } from "react"; -import { useDispatch } from "react-redux"; -import { setPresentationData, setStreaming } from "@/store/slices/presentationGeneration"; +import { useDispatch, useSelector } from "react-redux"; +import { clearPresentationData, setPresentationData, setStreaming } from "@/store/slices/presentationGeneration"; import { jsonrepair } from "jsonrepair"; +import { RootState } from "@/store/store"; export const usePresentationStreaming = ( presentationId: string, @@ -10,6 +11,8 @@ export const usePresentationStreaming = ( setError: (error: boolean) => void, fetchUserSlides: () => void ) => { + const { presentationData } = useSelector((state: RootState) => state.presentationGeneration); + const dispatch = useDispatch(); const previousSlidesLength = useRef(0); @@ -19,6 +22,7 @@ export const usePresentationStreaming = ( const initializeStream = async () => { dispatch(setStreaming(true)); + dispatch(clearPresentationData()); eventSource = new EventSource( `/api/v1/ppt/presentation/stream?presentation_id=${presentationId}` @@ -98,7 +102,9 @@ export const usePresentationStreaming = ( if (stream) { initializeStream(); } else { - fetchUserSlides(); + if(!presentationData || presentationData.slides.length === 0){ + fetchUserSlides(); + } } return () => { diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx index 90a1beb1..72d0525f 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx @@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react' import { File, X, Upload } from 'lucide-react' -import { useToast } from '@/hooks/use-toast' +import { toast } from 'sonner' import { cn } from '@/lib/utils' interface FileWithId extends File { @@ -17,7 +17,6 @@ interface SupportingDocProps { const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const [isDragging, setIsDragging] = useState(false) const fileInputRef = useRef(null) - const { toast } = useToast() // Convert Files to FileWithId with proper type checking const filesWithIds: FileWithId[] = files.map(file => { @@ -57,19 +56,15 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const invalidFiles = droppedFiles.filter(file => !validTypes.includes(file.type)); if (invalidFiles.length > 0) { - toast({ - title: 'Invalid file type', + toast.error('Invalid file type', { description: 'Please upload only PDF, TXT, PPTX, or DOCX files', - variant: 'destructive', }); return; } if (hasPdf && droppedFiles.some(file => file.type === 'application/pdf')) { - toast({ - title: 'Multiple PDF files are not allowed', + toast.error('Multiple PDF files are not allowed', { description: 'Please select only one PDF file', - variant: 'destructive', }); return; } @@ -82,8 +77,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const updatedFiles = [...files, ...validFiles] onFilesChange(updatedFiles) - toast({ - title: 'Files selected', + toast.success('Files selected', { description: `${validFiles.length} file(s) have been added`, }) } @@ -102,8 +96,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const updatedFiles = [...files, ...validFiles] onFilesChange(updatedFiles) - toast({ - title: 'Files selected', + toast.success('Files selected', { description: `${validFiles.length} file(s) have been added`, }) } diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/ToastTesting.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/ToastTesting.tsx new file mode 100644 index 00000000..e21ece2b --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/upload/components/ToastTesting.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import { Button } from '@/components/ui/button' +import { toast } from 'sonner' + +const ToastTesting = () => { + return ( +
+

Toast Testing - All Variants

+ +
+ {/* Success Toast */} + + + {/* Error Toast */} + + + {/* Info Toast */} + + + {/* Warning Toast */} + + + {/* Loading Toast */} + + + + + {/* Promise Toast */} + + + + + + + + + + + +
+ + +
+ ) +} + +export default ToastTesting diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index bd513217..52efb4ea 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -20,12 +20,12 @@ import { LanguageType, PresentationConfig } from "../type"; import SupportingDoc from "./SupportingDoc"; import { Button } from "@/components/ui/button"; import { ChevronRight } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { setPptGenUploadState } from "@/store/slices/presentationGenUpload"; -import { useLayout } from "../../context/LayoutContext"; +import ToastTesting from "./ToastTesting"; // Types for loading state interface LoadingState { @@ -39,7 +39,6 @@ interface LoadingState { const UploadPage = () => { const router = useRouter(); const dispatch = useDispatch(); - const { toast } = useToast(); // State management const [files, setFiles] = useState([]); @@ -72,18 +71,12 @@ const UploadPage = () => { */ const validateConfiguration = (): boolean => { if (!config.language || !config.slides) { - toast({ - title: "Please select number of Slides & Language", - variant: "destructive", - }); + toast.error("Please select number of Slides & Language"); return false; } if (!config.prompt.trim() && files.length === 0) { - toast({ - title: "No Prompt or Document Provided", - variant: "destructive", - }); + toast.error("No Prompt or Document Provided"); return false; } return true; @@ -177,10 +170,8 @@ const UploadPage = () => { duration: 0, showProgress: false, }); - toast({ - title: "Error", + toast.error("Error", { description: "Failed to generate presentation. Please try again.", - variant: "destructive", }); }; @@ -200,6 +191,7 @@ const UploadPage = () => { onConfigChange={handleConfigChange} /> +
{ - if (!fontName) { - return { link: '', name: '' }; - } - if (fontName.includes('instrument')) { - return { link: 'https://fonts.google.com/specimen/Instrument+Sans', name: 'Instrument Sans' } - } - if (fontName.includes('fraunces')) { - return { link: 'https://fonts.google.com/specimen/Fraunces', name: 'Fraunces' } - } - if (fontName.includes('montserrat')) { - return { link: 'https://fonts.google.com/specimen/Montserrat', name: 'Montserrat' } - } - if (fontName.includes('inria-serif')) { - return { link: 'https://fonts.google.com/specimen/Inria+Serif', name: 'Inria Serif' } - } - if (fontName.includes('inter')) { - return { link: 'https://fonts.google.com/specimen/Inter', name: 'Inter' } - } - else { - return { link: '', name: '' }; - } -} -export const numberTranslations: any = { - // Key languages - "English (English)": ["01", "02", "03", "04", "05"], - "English(English)": ["01", "02", "03", "04", "05"], - English: ["01", "02", "03", "04", "05"], - "Spanish (Español)": ["01", "02", "03", "04", "05"], - "French (Français)": ["01", "02", "03", "04", "05"], - "German (Deutsch)": ["01", "02", "03", "04", "05"], - "Portuguese (Português)": ["01", "02", "03", "04", "05"], - "Italian (Italiano)": ["01", "02", "03", "04", "05"], - "Dutch (Nederlands)": ["01", "02", "03", "04", "05"], - "Russian (Русский)": ["01", "02", "03", "04", "05"], - "Chinese (Simplified & Traditional - 中文, 汉语/漢語)": [ - "一", - "二", - "三", - "四", - "五", - ], - "Japanese (日本語)": ["一", "二", "三", "四", "五"], - "Korean (한국어)": ["일", "이", "삼", "사", "오"], - "Arabic (العربية)": ["١", "٢", "٣", "٤", "٥"], - "Hindi (हिन्दी)": ["०१", "०२", "०३", "०४", "०५"], - "Bengali (বাংলা)": ["০১", "০২", "০৩", "০৪", "০৫"], - - // European Languages - "Polish (Polski)": ["01", "02", "03", "04", "05"], - "Czech (Čeština)": ["01", "02", "03", "04", "05"], - "Slovak (Slovenčina)": ["01", "02", "03", "04", "05"], - "Hungarian (Magyar)": ["01", "02", "03", "04", "05"], - "Romanian (Română)": ["01", "02", "03", "04", "05"], - "Bulgarian (Български)": ["01", "02", "03", "04", "05"], - "Greek (Ελληνικά)": ["α΄", "β΄", "γ΄", "δ΄", "ε΄"], - "Serbian (Српски)": ["01", "02", "03", "04", "05"], - "Croatian (Hrvatski)": ["01", "02", "03", "04", "05"], - "Bosnian (Bosanski)": ["01", "02", "03", "04", "05"], - "Slovenian (Slovenščina)": ["01", "02", "03", "04", "05"], - "Finnish (Suomi)": ["01", "02", "03", "04", "05"], - "Swedish (Svenska)": ["01", "02", "03", "04", "05"], - "Danish (Dansk)": ["01", "02", "03", "04", "05"], - "Norwegian (Norsk)": ["01", "02", "03", "04", "05"], - "Icelandic (Íslenska)": ["01", "02", "03", "04", "05"], - "Lithuanian (Lietuvių)": ["01", "02", "03", "04", "05"], - "Latvian (Latviešu)": ["01", "02", "03", "04", "05"], - "Estonian (Eesti)": ["01", "02", "03", "04", "05"], - "Maltese (Malti)": ["01", "02", "03", "04", "05"], - "Welsh (Cymraeg)": ["01", "02", "03", "04", "05"], - "Irish (Gaeilge)": ["01", "02", "03", "04", "05"], - "Scottish Gaelic (Gàidhlig)": ["01", "02", "03", "04", "05"], - - // Middle Eastern and Central Asian Languages - "Hebrew (עברית)": ["א׳", "ב׳", "ג׳", "ד׳", "ה׳"], - "Persian/Farsi (فارسی)": ["۱", "۲", "۳", "۴", "۵"], - "Turkish (Türkçe)": ["01", "02", "03", "04", "05"], - "Kurdish (Kurdî / کوردی)": ["١", "٢", "٣", "٤", "٥"], - "Pashto (پښتو)": ["١", "٢", "٣", "٤", "٥"], - "Dari (دری)": ["١", "٢", "٣", "٤", "٥"], - "Uzbek (Oʻzbek)": ["01", "02", "03", "04", "05"], - "Kazakh (Қазақша)": ["01", "02", "03", "04", "05"], - "Tajik (Тоҷикӣ)": ["01", "02", "03", "04", "05"], - "Turkmen (Türkmençe)": ["01", "02", "03", "04", "05"], - "Azerbaijani (Azərbaycan dili)": ["01", "02", "03", "04", "05"], - - // South Asian Languages - "Urdu (اردو)": ["١", "٢", "٣", "٤", "٥"], - "Tamil (தமிழ்)": ["௧", "௨", "௩", "௪", "௫"], - "Telugu (తెలుగు)": ["౧", "౨", "౩", "౪", "౫"], - "Marathi (मराठी)": ["०१", "०२", "०३", "०४", "०५"], - "Punjabi (ਪੰਜਾਬੀ / پنجابی)": ["੦੧", "੦੨", "੦੩", "੦੪", "੦੫"], - "Gujarati (ગુજરાતી)": ["૦૧", "૦૨", "૦૩", "૦૪", "૦૫"], - "Malayalam (മലയാളം)": ["൧", "൨", "൩", "൪", "൫"], - "Kannada (ಕನ್ನಡ)": ["೧", "೨", "೩", "೪", "೫"], - "Odia (ଓଡ଼ିଆ)": ["୧", "୨", "୩", "୪", "୫"], - "Sinhala (සිංහල)": ["෧", "෨", "෩", "෪", "෫"], - "Nepali (नेपाली)": ["०१", "०२", "०३", "०४", "०५"], - - // East and Southeast Asian Languages - "Thai (ไทย)": ["๑", "๒", "๓", "๔", "๕"], - "Vietnamese (Tiếng Việt)": ["01", "02", "03", "04", "05"], - "Lao (ລາວ)": ["໑", "໒", "໓", "໔", "໕"], - "Khmer (ភាសាខ្មែរ)": ["១", "២", "៣", "៤", "៥"], - "Burmese (မြန်မာစာ)": ["၁", "၂", "၃", "၄", "၅"], - "Tagalog/Filipino (Tagalog/Filipino)": ["01", "02", "03", "04", "05"], - "Javanese (Basa Jawa)": ["01", "02", "03", "04", "05"], - "Sundanese (Basa Sunda)": ["01", "02", "03", "04", "05"], - "Malay (Bahasa Melayu)": ["01", "02", "03", "04", "05"], - "Mongolian (Монгол)": ["01", "02", "03", "04", "05"], - - // African Languages - "Swahili (Kiswahili)": ["01", "02", "03", "04", "05"], - "Hausa (Hausa)": ["01", "02", "03", "04", "05"], - "Yoruba (Yoruba)": ["01", "02", "03", "04", "05"], - "Igbo (Igbo)": ["01", "02", "03", "04", "05"], - "Amharic (አማርኛ)": ["፩", "፪", "፫", "፬", "፭"], - "Zulu (isiZulu)": ["01", "02", "03", "04", "05"], - "Xhosa (isiXhosa)": ["01", "02", "03", "04", "05"], - "Shona (ChiShona)": ["01", "02", "03", "04", "05"], - "Somali (Soomaaliga)": ["01", "02", "03", "04", "05"], - - // Indigenous and Lesser-Known Languages - "Basque (Euskara)": ["01", "02", "03", "04", "05"], - "Catalan (Català)": ["01", "02", "03", "04", "05"], - "Galician (Galego)": ["01", "02", "03", "04", "05"], - "Quechua (Runasimi)": ["01", "02", "03", "04", "05"], - "Nahuatl (Nāhuatl)": ["01", "02", "03", "04", "05"], - "Hawaiian (ʻŌlelo Hawaiʻi)": ["01", "02", "03", "04", "05"], - "Maori (Te Reo Māori)": ["01", "02", "03", "04", "05"], - "Tahitian (Reo Tahiti)": ["01", "02", "03", "04", "05"], - "Samoan (Gagana Samoa)": ["01", "02", "03", "04", "05"], -}; - -export const ThemeImagePrompt = { - light: - "Classy and modern with a corporate and minimalist touch. Tone is serious yet elegant, using a palette of light, white, and cool gray colors.", - dark: "Luxurious and futuristic with a simple, clean design. Professional yet elegant using a color scheme of dark, black, and high contrast.", - faint_yellow: - "Fresh young creatively vibrant style, utilizing a playful mixture of light colors like orange, salmon, and pastel purple, all set against a warm gradient.", - cream: - "Elegant with a classic and professional look. Subtle and minimalist using a warm palette of cream, beige, and light beige colors.", - royal_blue: - "Playful and creative, bold and loud with a futuristic touch, using a gradient of vibrant colors including blue, purple, and royal blue.", - light_red: - "Fun and organic with a playful and inspirational aesthetic, featuring pastel colors like pink, coral, and orange for a vibrant and warm feel.", - dark_pink: - " Inspirational and creative with a youthful and playful tone, featuring light, pastel colors including blue, pink, and purple, all blending in a vibrant gradient.", - custom: "", -}; export function sanitizeFilename(input: string, replacement = '') { diff --git a/servers/nextjs/app/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/dashboard/components/PresentationCard.tsx index 720bce3f..55294912 100644 --- a/servers/nextjs/app/dashboard/components/PresentationCard.tsx +++ b/servers/nextjs/app/dashboard/components/PresentationCard.tsx @@ -9,7 +9,7 @@ import { PopoverContent, } from "@/components/ui/popover"; import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts"; export const PresentationCard = ({ @@ -39,28 +39,20 @@ export const PresentationCard = ({ e.preventDefault(); e.stopPropagation(); - toast({ - title: "Deleting presentation", + toast.loading("Deleting presentation", { description: "Please wait while we delete the presentation", - variant: "default", }); const response = await DashboardApi.deletePresentation(id); if (response) { - toast({ - title: "Presentation deleted", + toast.success("Presentation deleted", { description: "The presentation has been deleted successfully", - variant: "default", }); if (onDeleted) { onDeleted(id); } } else { - toast({ - title: "Error", - description: "Failed to delete presentation", - variant: "destructive", - }); + toast.error("Error deleting presentation"); } }; return ( diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index 856f5b54..00a14e2a 100644 --- a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' -import { toast } from '@/hooks/use-toast' +import { toast } from 'sonner' interface UseGroupLayoutLoaderReturn { layoutGroup: LayoutGroup | null @@ -42,8 +42,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet const response = await fetch('/api/layouts') if (!response.ok) { - toast({ - title: 'Error loading layouts', + toast.error('Error loading layouts', { description: response.statusText, }) return @@ -75,8 +74,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) if (!module.default) { - toast({ - title: `${layoutName} has no default export`, + toast.error(`${layoutName} has no default export`, { description: 'Please ensure the layout file exports a default component', }) console.warn(`${layoutName} has no default export`) @@ -84,8 +82,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } if (!module.Schema) { - toast({ - title: `${layoutName} is missing required Schema export`, + toast.error(`${layoutName} is missing required Schema export`, { description: 'Please ensure the layout file exports a Schema', }) console.error(`${layoutName} is missing required Schema export`) @@ -139,8 +136,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } if (groupLayouts.length === 0) { - toast({ - title: 'No valid layouts found', + toast.error('No valid layouts found', { description: `No valid layouts found in "${groupSlug}" group.`, }) setError(`No valid layouts found in "${groupSlug}" group.`) diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index 20673e69..3d4e3a2d 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' -import { toast } from '@/hooks/use-toast' +import { toast } from 'sonner' interface UseLayoutLoaderReturn { layoutGroups: LayoutGroup[] @@ -25,8 +25,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const response = await fetch('/api/layouts') if (!response.ok) { - toast({ - title: 'Error loading layouts', + toast.error('Error loading layouts', { description: response.statusText, }) return @@ -50,21 +49,16 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (!module.default) { - toast({ - title: `${layoutName} has no default export`, + toast.error(`${layoutName} has no default export`, { description: 'Please ensure the layout file exports a default component', - }) console.warn(`${layoutName} has no default export`) continue } if (!module.Schema) { - toast({ - title: `${layoutName} is missing required Schema export`, + toast.error(`${layoutName} is missing required Schema export`, { description: 'Please ensure the layout file exports a Schema', - - }) console.error(`${layoutName} is missing required Schema export`) continue @@ -130,10 +124,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } if (allLayouts.length === 0) { - toast({ - title: 'No valid layouts found', + toast.error('No valid layouts found', { description: 'Make sure your layout files export both a default component and a Schema.', - }) setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.') } else { diff --git a/servers/nextjs/app/layout.tsx b/servers/nextjs/app/layout.tsx index d8ea12de..a579f8c4 100644 --- a/servers/nextjs/app/layout.tsx +++ b/servers/nextjs/app/layout.tsx @@ -3,9 +3,9 @@ import localFont from "next/font/local"; import { Fraunces, Montserrat, Inria_Serif, Roboto, Instrument_Sans } from "next/font/google"; import "./globals.css"; import { Providers } from "./providers"; -import { Toaster } from "@/components/ui/toaster"; import { FooterProvider } from "./(presentation-generator)/context/footerContext"; import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext"; +import { Toaster } from "sonner"; const fraunces = Fraunces({ subsets: ["latin"], @@ -105,13 +105,11 @@ export default function RootLayout({ - - {children} - + ); diff --git a/servers/nextjs/app/settings/SettingPage.tsx b/servers/nextjs/app/settings/SettingPage.tsx index c7c39f9d..d70d90a7 100644 --- a/servers/nextjs/app/settings/SettingPage.tsx +++ b/servers/nextjs/app/settings/SettingPage.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import Header from "../dashboard/components/Header"; import Wrapper from "@/components/Wrapper"; import { Settings, Key, Loader2, Check, ChevronsUpDown } from "lucide-react"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { RootState } from "@/store/store"; import { useSelector } from "react-redux"; import { handleSaveLLMConfig } from "@/utils/storeHelpers"; @@ -156,22 +156,16 @@ const SettingsPage = () => { setIsLoading(true); await pullOllamaModels(); } - toast({ - title: "Success", - description: "Configuration saved successfully", - }); + toast.success("Configuration saved successfully"); setIsLoading(false); router.back(); } catch (error) { console.error("Error:", error); - toast({ - title: "Error", - description: - error instanceof Error - ? error.message - : "Failed to save configuration", - variant: "destructive", - }); + toast.error( + error instanceof Error + ? error.message + : "Failed to save configuration" + ); setIsLoading(false); } }; @@ -284,23 +278,18 @@ const SettingsPage = () => { const isModelAvailable = data.includes(llmConfig.CUSTOM_MODEL); if (!isModelAvailable) { setLlmConfig({ ...llmConfig, CUSTOM_MODEL: "" }); - toast({ - title: "Model Unavailable", - description: `The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.`, - variant: "destructive", - }); + toast.error( + `The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.` + ); } } } catch (error) { console.error("Error fetching custom models:", error); // Don't set customModelsChecked to true on error, so the button remains visible setCustomModels([]); - toast({ - title: "Error", - description: - "Failed to fetch available models. Please check your URL and API key.", - variant: "destructive", - }); + toast.error( + "Failed to fetch available models. Please check your URL and API key." + ); } finally { setCustomModelsLoading(false); } @@ -385,15 +374,15 @@ const SettingsPage = () => { key={provider} onClick={() => changeProvider(provider)} className={`relative p-4 rounded-lg border-2 transition-all duration-200 ${llmConfig.LLM === provider - ? "border-blue-500 bg-blue-50" - : "border-gray-200 hover:border-blue-200 hover:bg-gray-50" + ? "border-blue-500 bg-blue-50" + : "border-gray-200 hover:border-blue-200 hover:bg-gray-50" }`} >
{provider === "openai" @@ -770,8 +759,8 @@ const SettingsPage = () => { customModelsLoading || !llmConfig.CUSTOM_LLM_URL } className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 font-semibold ${customModelsLoading || !llmConfig.CUSTOM_LLM_URL - ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" - : "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20" + ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" + : "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20" }`} > {customModelsLoading ? ( @@ -804,8 +793,8 @@ const SettingsPage = () => { customModelsLoading || !llmConfig.CUSTOM_LLM_URL } className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 font-semibold ${customModelsLoading || !llmConfig.CUSTOM_LLM_URL - ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" - : "bg-white border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-2 focus:ring-gray-500/20" + ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" + : "bg-white border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-2 focus:ring-gray-500/20" }`} > {customModelsLoading ? ( @@ -995,10 +984,10 @@ const SettingsPage = () => { (llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) } className={`mt-8 w-full font-semibold py-3 px-4 rounded-lg transition-all duration-500 ${isLoading || - (llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) || - (llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) - ? "bg-gray-400 cursor-not-allowed" - : "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200" + (llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) || + (llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) + ? "bg-gray-400 cursor-not-allowed" + : "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200" } text-white`} > {isLoading ? ( diff --git a/servers/nextjs/app/settings/page.tsx b/servers/nextjs/app/settings/page.tsx index 1aca4ed3..e60eb189 100644 --- a/servers/nextjs/app/settings/page.tsx +++ b/servers/nextjs/app/settings/page.tsx @@ -1,6 +1,12 @@ import React from 'react' import SettingPage from './SettingPage' + +export const metadata = { + title: 'Settings | Presenton', + description: 'Settings page', +} const page = () => { + return ( ) diff --git a/servers/nextjs/components/Home.tsx b/servers/nextjs/components/Home.tsx index 8df8cc13..deeea0a3 100644 --- a/servers/nextjs/components/Home.tsx +++ b/servers/nextjs/components/Home.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { Info, ExternalLink, @@ -276,22 +276,20 @@ export default function Home() { setIsLoading(true); await pullOllamaModels(); } - toast({ - title: "Success", - description: "Configuration saved successfully", - }); + toast.success("Configuration saved successfully"); setIsLoading(false); router.push("/upload"); } catch (error) { console.error("Error:", error); - toast({ - title: "Error", - description: - error instanceof Error - ? error.message - : "Failed to save configuration", - variant: "destructive", - }); + toast.error( + error instanceof Error + ? error.message + : "Failed to save configuration", + { + description: + "Failed to save configuration", + } + ); setIsLoading(false); } }; @@ -413,12 +411,13 @@ export default function Home() { console.error("Error fetching custom models:", error); // Don't set customModelsChecked to true on error, so the button remains visible setCustomModels([]); - toast({ - title: "Error", - description: - "Failed to fetch available models. Please check your URL and API key.", - variant: "destructive", - }); + toast.error( + "Failed to fetch available models. Please check your URL and API key.", + { + description: + "Failed to fetch available models. Please check your URL and API key.", + } + ); } finally { setCustomModelsLoading(false); } diff --git a/servers/nextjs/components/ui/sonner.tsx b/servers/nextjs/components/ui/sonner.tsx index 452f4d9f..261ee62d 100644 --- a/servers/nextjs/components/ui/sonner.tsx +++ b/servers/nextjs/components/ui/sonner.tsx @@ -12,6 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => { , - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-green-50 text-green-700 ", - success: "border bg-green-50 text-green-700 border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive/90 text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} diff --git a/servers/nextjs/components/ui/toaster.tsx b/servers/nextjs/components/ui/toaster.tsx deleted file mode 100644 index 171beb46..00000000 --- a/servers/nextjs/components/ui/toaster.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"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() - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ) - })} - -
- ) -} diff --git a/servers/nextjs/hooks/use-toast.ts b/servers/nextjs/hooks/use-toast.ts deleted file mode 100644 index 02e111d8..00000000 --- a/servers/nextjs/hooks/use-toast.ts +++ /dev/null @@ -1,194 +0,0 @@ -"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 }