Skip to content

Commit

Permalink
Error toast system (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
jarrod-lowe authored Sep 21, 2024
1 parent daab461 commit 51d2e68
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 12 deletions.
74 changes: 67 additions & 7 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"react-dom": "18.3.1",
"react-icons": "5.3.0",
"react-intl": "6.6.8",
"react-router-dom": "6.26.2"
"react-router-dom": "6.26.2",
"uuid": "10.0.0"
},
"scripts": {
"dev": "vite",
Expand Down
46 changes: 46 additions & 0 deletions ui/public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,49 @@ button:hover {
border: 1px solid #ddd;
border-radius: 4px;
}

/* NotificationToast.css */

.toast-manager {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px; /* Space between each toast */
z-index: 1000; /* Make sure it's on top of other UI elements */
}

.notification-toast {
background-color: rgba(255 0 0 90%); /* For errors: red background */
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0 0 0 10%);
animation: slide-in 0.3s ease-out;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
opacity: 1;
max-width: 300px;
}

.notification-toast.success {
background-color: rgba(0 128 0 90%); /* For success: green background */
}

.notification-toast.fade-out {
opacity: 0;
transform: translateY(-20px); /* Move up when fading out */
transition: opacity 0.3s ease-in, transform 0.3s ease-in;
}

@keyframes slide-in {
from {
opacity: 0;
transform: translateY(20px);
}

to {
opacity: 1;
transform: translateY(0);
}
}
5 changes: 4 additions & 1 deletion ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TopBar } from "./frame";
import { generateClient, GraphQLResult } from "aws-amplify/api";
import { joinGameMutation } from "../../appsync/schema";
import type { Game } from "../../appsync/graphql";
import { ToastProvider } from "./notificationToast";

const GamesMenu = React.lazy(() => import("./gamesMenu"))
const AppGame = React.lazy(() => import("./game"))
Expand Down Expand Up @@ -178,7 +179,9 @@ interface CustomError extends Error {
export function App() {
return (
<IntlProvider messages={messages['en']} locale="en" defaultLocale="en">
<AppContent />
<ToastProvider>
<AppContent />
</ToastProvider>
</IntlProvider>
);
}
Expand Down
114 changes: 114 additions & 0 deletions ui/src/notificationToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { createContext, useContext, useEffect, useState, useMemo, ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid';

const MaxToasts = 5;

interface ToastContextType {
addToast: (message: string, type: 'error' | 'success') => void;
}

// Create the context with a default value
const ToastContext = createContext<ToastContextType | undefined>(undefined);

// Custom hook to use the ToastContext
export const useToast = (): ToastContextType => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};

// ToastProvider component that wraps the ToastManager
export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<{ id: string, message: string, type: 'error' | 'success' }[]>([]);

// Function to add a toast
const addToast = (message: string, type: 'error' | 'success') => {
setToasts((currentToasts) => {
// Remove the oldest toast if we've reached the maximum number
if (currentToasts.length >= MaxToasts) {
currentToasts.shift(); // Remove the first (oldest) toast
}
return [...currentToasts, { id: uuidv4(), message, type }];
});
};

// Function to remove a toast
const removeToast = (id: string) => {
setToasts((currentToasts) => currentToasts.filter((toast) => toast.id !== id));
};

// Memoize the context value so it doesn't change on every render
const contextValue = useMemo(() => ({ addToast }), [toasts]);

return (
<ToastContext.Provider value={contextValue}>
{children}
<ToastManager toasts={toasts} removeToast={removeToast} /> {/* Render ToastManager here */}
</ToastContext.Provider>
);
};

interface Toast {
id: string;
message: string;
type: 'error' | 'success';
}

interface ToastManagerProps {
toasts: Toast[];
removeToast: (id: string) => void;
}

export const ToastManager: React.FC<ToastManagerProps> = ({ toasts, removeToast }) => {
return (
<div className="toast-manager">
{toasts.map((toast) => (
<NotificationToast
key={toast.id} // Unique key
message={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)} // Close specific toast
/>
))}
</div>
);
};

interface NotificationToastProps {
message: string;
type: 'error' | 'success';
duration?: number;
onClose?: () => void;
}

export const NotificationToast: React.FC<NotificationToastProps> = ({ message, type, duration = 5000, onClose }) => {
const [isFadingOut, setIsFadingOut] = useState(false);

useEffect(() => {
// Start fading out slightly before the toast is removed
const fadeOutTimer = setTimeout(() => {
setIsFadingOut(true); // Start fade out animation
}, duration - 300); // Start fading out 300ms before the full duration

// Remove the toast completely after the full duration
const removeTimer = setTimeout(() => {
if (onClose) {
onClose(); // Remove toast after animation completes
}
}, duration);

// Cleanup timers on component unmount or re-render
return () => {
clearTimeout(fadeOutTimer);
clearTimeout(removeTimer);
};
}, [duration, onClose]);

return (
<div className={`notification-toast ${type} ${isFadingOut ? 'fade-out' : ''}`}>
<p>{message}</p>
</div>
);
};
6 changes: 5 additions & 1 deletion ui/src/sectionNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { SheetSection, UpdateSectionInput } from "../../appsync/graphql";
import { generateClient } from "aws-amplify/api";
import { GraphQLResult } from "@aws-amplify/api-graphql";
import { updateSectionMutation } from '../../appsync/schema';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { useToast } from './notificationToast';

export const SectionNumber: React.FC<{ section: SheetSection, userSubject: string, onUpdate: (updatedSection: SheetSection) => void }> = ({ section, userSubject, onUpdate }) => {
const intl = useIntl();
const toast = useToast();
const content = JSON.parse(section.content) as { number: number };

const handleIncrement = async () => {
Expand All @@ -26,6 +29,7 @@ export const SectionNumber: React.FC<{ section: SheetSection, userSubject: strin
onUpdate(response.data.updateSection);
} catch (error) {
console.error("Error updating section:", error);
toast.addToast(intl.formatMessage({ id: "sectionNumber.updateError" }), 'error');
}
};

Expand Down
6 changes: 5 additions & 1 deletion ui/src/sectionText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { updateSectionMutation } from "../../appsync/schema";
import { FormattedMessage, useIntl } from 'react-intl';
import { GraphQLResult } from "@aws-amplify/api-graphql";
import { FaPencilAlt } from 'react-icons/fa';
import { useToast } from './notificationToast';

type SectionTypeText = {
text: string;
Expand All @@ -16,6 +17,7 @@ export const SectionText: React.FC<{ section: SheetSection, userSubject: string,
const [content, setContent] = useState(JSON.parse(section.content) as SectionTypeText);
const [sectionName, setSectionName] = useState(section.sectionName);
const intl = useIntl(); // Get the intl object for translation
const toast = useToast();

const handleUpdate = async () => {
try {
Expand All @@ -37,6 +39,8 @@ export const SectionText: React.FC<{ section: SheetSection, userSubject: string,
setIsEditing(false);
} catch (error) {
console.error("Error updating section:", error);
toast.addToast(intl.formatMessage({ id: "sectionText.updateError" }), 'error');

}
};

Expand All @@ -61,7 +65,7 @@ export const SectionText: React.FC<{ section: SheetSection, userSubject: string,
<textarea
value={content.text}
onChange={(e) => setContent({ ...content, text: e.target.value })}
placeholder={intl.formatMessage({ id: "sectionContent.text" })}
placeholder={intl.formatMessage({ id: "sectionText.sampleContent" })}
/>
<button onClick={handleUpdate}>
<FormattedMessage id="save" />
Expand Down
Loading

0 comments on commit 51d2e68

Please sign in to comment.