refactor(Nextjs): Remove toast add sonner with different varient & colors

This commit is contained in:
shiva raj badu 2025-07-20 00:38:24 +05:45
parent b2210cdaac
commit e04fdc5558
No known key found for this signature in database
26 changed files with 241 additions and 746 deletions

View file

@ -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<boolean>(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;
}

View file

@ -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`);

View file

@ -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,

View file

@ -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");
}
};

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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,
};
};

View file

@ -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 () => {

View file

@ -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<HTMLInputElement>(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`,
})
}

View file

@ -0,0 +1,115 @@
import React from 'react'
import { Button } from '@/components/ui/button'
import { toast } from 'sonner'
const ToastTesting = () => {
return (
<div className="p-8 space-y-4">
<h2 className="text-2xl font-bold mb-6">Toast Testing - All Variants</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{/* Success Toast */}
<Button
onClick={() => toast.success('Success! Operation completed successfully', {
description: 'Your data has been saved.',
duration: 4000,
richColors: true,
})}
className="bg-green-600 hover:bg-green-700"
>
Success Toast
</Button>
{/* Error Toast */}
<Button
onClick={() => toast.error('Error! Something went wrong', {
description: 'Please try again later.',
duration: 5000,
richColors: true,
})}
className="bg-red-600 hover:bg-red-700"
>
Error Toast
</Button>
{/* Info Toast */}
<Button
onClick={() => toast.info('Information', {
description: 'Here is some useful information for you.',
duration: 4000,
richColors: true,
})}
className="bg-blue-600 hover:bg-blue-700"
>
Info Toast
</Button>
{/* Warning Toast */}
<Button
onClick={() => toast.warning('Warning! Please be careful', {
description: 'This action cannot be undone.',
duration: 4000,
richColors: true,
})}
className="bg-yellow-600 hover:bg-yellow-700"
>
Warning Toast
</Button>
{/* Loading Toast */}
<Button
onClick={() => {
const loadingToast = toast.loading('Processing...', {
description: 'Please wait while we process your request.',
});
// Simulate loading completion after 3 seconds
setTimeout(() => {
toast.dismiss(loadingToast);
toast.success('Processing completed!');
}, 3000);
}}
className="bg-indigo-600 hover:bg-indigo-700"
>
Loading Toast
</Button>
{/* Promise Toast */}
<Button
onClick={() => {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve('Success!') : reject('Failed!');
}, 2000);
});
toast.promise(promise, {
loading: 'Uploading file...',
success: 'File uploaded successfully!',
error: 'Failed to upload file',
});
}}
className="bg-teal-600 hover:bg-teal-700"
>
Promise Toast
</Button>
</div>
</div>
)
}
export default ToastTesting

View file

@ -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<File[]>([]);
@ -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}
/>
</div>
<ToastTesting />
<div className="relative">
<PromptInput
value={config.prompt}

View file

@ -52,170 +52,10 @@ export function removeUUID(fileName: string) {
}
export function generateRandomId(): string {
const length = 36;
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
let id = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * chars.length);
id += chars[randomIndex];
}
return id;
}
export const getFontLink = (fontName: string) => {
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 = '') {

View file

@ -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 (

View file

@ -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.`)

View file

@ -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 {

View file

@ -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({
<Providers>
<LayoutProvider>
<FooterProvider>
{children}
</FooterProvider>
</LayoutProvider>
</Providers>
<Toaster />
<Toaster position="top-center" />
</body>
</html>
);

View file

@ -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"
}`}
>
<div className="flex items-center justify-center gap-3">
<span
className={`font-medium text-center ${llmConfig.LLM === provider
? "text-blue-700"
: "text-gray-700"
? "text-blue-700"
: "text-gray-700"
}`}
>
{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 ? (

View file

@ -1,6 +1,12 @@
import React from 'react'
import SettingPage from './SettingPage'
export const metadata = {
title: 'Settings | Presenton',
description: 'Settings page',
}
const page = () => {
return (
<SettingPage />
)

View file

@ -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);
}

View file

@ -12,6 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
duration={2000}
richColors={true}
toastOptions={{
classNames: {
toast:

View file

@ -1,129 +0,0 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Cross2Icon } from "@radix-ui/react-icons"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-2 z-[100] flex max-h-screen w-full flex-col-reverse p-4 right-0 sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
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<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-bold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm font-medium opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View file

@ -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 (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View file

@ -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<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
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<ToasterToast, "id">
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<State>(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 }