diff --git a/src/app/Navbar.jsx b/src/app/Navbar.jsx
index cc7c76c..af9add0 100644
--- a/src/app/Navbar.jsx
+++ b/src/app/Navbar.jsx
@@ -3,6 +3,7 @@ import NewTypeModal from "./components/modals/NewTypeModal";
import ImportModal from "./components/modals/ImportModal";
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
+import { toast } from 'react-toastify';
import { useClickOutside } from './hooks/useClickOutside';
const constants = require('./constants');
const theme = require('../styling/theme');
@@ -133,9 +134,9 @@ const NavbarFunction = ({user, setUserChanged, newTypes, isAdmin}) => {
const onCreateNewType = (newName) => {
newName = newName.trim().toLowerCase().replace(/ /g, '-');
if(user.newTypes[newName]) {
- window.alert('Type Already Exists');
+ toast.error('Type already exists');
} else if(Object.keys(user.newTypes).length === constants.maxCustomTypes) {
- window.alert(`Maximum custom types reached (${constants.maxCustomTypes})`);
+ toast.error(`Maximum custom types reached (${constants.maxCustomTypes})`);
} else {
axios
.put(constants['SERVER_URL'] + `/api/user/newTypes`, {newType: newName})
@@ -144,7 +145,7 @@ const NavbarFunction = ({user, setUserChanged, newTypes, isAdmin}) => {
setPendingNewType(newName);
setUserChanged(true);
})
- .catch(() => window.alert("Error on Create New Type"));
+ .catch(() => toast.error("Error creating new type"));
}
};
@@ -163,11 +164,11 @@ const NavbarFunction = ({user, setUserChanged, newTypes, isAdmin}) => {
link.click();
document.body.removeChild(link);
} else {
- window.alert('Failed to export data');
+ toast.error('Failed to export data');
}
} catch (error) {
console.error('Export error:', error);
- window.alert('Error exporting data');
+ toast.error('Error exporting data');
}
};
diff --git a/src/app/Navbar.test.jsx b/src/app/Navbar.test.jsx
index 4d4a62b..cc52984 100644
--- a/src/app/Navbar.test.jsx
+++ b/src/app/Navbar.test.jsx
@@ -1,8 +1,16 @@
import { renderWithRouter, screen, fireEvent, waitFor } from '../test-utils';
import axios from 'axios';
import Navbar from './Navbar';
+import { toast } from 'react-toastify';
jest.mock('axios');
+jest.mock('react-toastify', () => ({
+ toast: {
+ error: jest.fn(),
+ success: jest.fn(),
+ },
+ ToastContainer: () => null,
+}));
jest.mock('./components/modals/NewTypeModal', () => ({ show }) =>
show ?
: null
);
@@ -30,7 +38,6 @@ beforeEach(() => {
jest.useFakeTimers();
axios.get.mockResolvedValue({ data: { success: true, incoming: [] } });
axios.put.mockResolvedValue({});
- window.alert = jest.fn();
});
afterEach(() => {
@@ -138,6 +145,6 @@ describe('Navbar', () => {
// Modal is mocked, so simulate the onSaveClick by accessing internal state is not possible
// Instead test by calling the handler directly through the modal interaction stub
// This test verifies the alert path exists — covered indirectly by the component
- expect(window.alert).not.toHaveBeenCalled(); // sanity — no alert yet
+ expect(toast.error).not.toHaveBeenCalled(); // sanity — no toast yet
});
});
diff --git a/src/app/components/modals/ShareLinkModal.jsx b/src/app/components/modals/ShareLinkModal.jsx
index 3b15be1..558b56a 100644
--- a/src/app/components/modals/ShareLinkModal.jsx
+++ b/src/app/components/modals/ShareLinkModal.jsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
+import { toast } from 'react-toastify';
import { toCapitalNotation } from '../../helpers';
import Modal from './Modal';
const constants = require('../../constants');
@@ -70,7 +71,7 @@ function ShareLinkModal({
.catch(err => {
console.error(err);
setIsGeneratingLink(false);
- window.alert('Error generating link');
+ toast.error('Error generating link');
});
}
@@ -88,7 +89,7 @@ function ShareLinkModal({
})
.catch(err => {
console.error(err);
- window.alert('Error revoking link');
+ toast.error('Error revoking link');
});
}
diff --git a/src/app/components/modals/ShareLinkModal.test.jsx b/src/app/components/modals/ShareLinkModal.test.jsx
index f8ccdd6..41163cd 100644
--- a/src/app/components/modals/ShareLinkModal.test.jsx
+++ b/src/app/components/modals/ShareLinkModal.test.jsx
@@ -1,8 +1,16 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import axios from 'axios';
import ShareLinkModal from './ShareLinkModal';
+import { toast } from 'react-toastify';
jest.mock('axios');
+jest.mock('react-toastify', () => ({
+ toast: {
+ error: jest.fn(),
+ success: jest.fn(),
+ },
+ ToastContainer: () => null,
+}));
const defaultProps = {
show: true,
@@ -26,7 +34,7 @@ beforeEach(() => {
configurable: true,
});
window.confirm = jest.fn(() => true);
- window.alert = jest.fn();
+ toast.error.mockClear();
});
describe('ShareLinkModal', () => {
diff --git a/src/app/pages/CreateMedia.jsx b/src/app/pages/CreateMedia.jsx
index 95d7560..3766879 100644
--- a/src/app/pages/CreateMedia.jsx
+++ b/src/app/pages/CreateMedia.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Link, useParams, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
+import { toast } from 'react-toastify';
import PageMeta from '../components/ui/PageMeta';
import TagMaker from "../components/TagMaker";
import { toCapitalNotation } from "../helpers";
@@ -183,7 +184,7 @@ const CreateMedia = ({user, toDo, newType, selectedTags, dataSource = 'api', bas
navigate(-1);
})
.catch((err) => {
- window.alert("Create Failed :(")
+ toast.error("Create failed");
});
};
diff --git a/src/app/pages/CreateMedia.test.jsx b/src/app/pages/CreateMedia.test.jsx
index c7cbc0e..ed3af5f 100644
--- a/src/app/pages/CreateMedia.test.jsx
+++ b/src/app/pages/CreateMedia.test.jsx
@@ -4,8 +4,16 @@ import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import axios from 'axios';
import CreateMedia from './CreateMedia';
+import { toast } from 'react-toastify';
jest.mock('axios');
+jest.mock('react-toastify', () => ({
+ toast: {
+ error: jest.fn(),
+ success: jest.fn(),
+ },
+ ToastContainer: () => null,
+}));
jest.mock('../components/TagMaker', () => () =>
);
const mockUser = {
@@ -44,7 +52,7 @@ function renderCreateMedia(props = {}) {
beforeEach(() => {
jest.clearAllMocks();
axios.post.mockResolvedValue({ data: {} });
- window.alert = jest.fn();
+ toast.error.mockClear();
});
describe('CreateMedia', () => {
diff --git a/src/app/pages/Friends.jsx b/src/app/pages/Friends.jsx
index 75007bc..12a046b 100644
--- a/src/app/pages/Friends.jsx
+++ b/src/app/pages/Friends.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
import PageMeta from '../components/ui/PageMeta';
const constants = require('../constants');
const theme = require('../../styling/theme');
@@ -56,7 +57,7 @@ function Friends({ user, setUserChanged }) {
}
} catch (err) {
console.error('Error accepting friend request:', err);
- window.alert(err.response?.data?.message || 'Failed to accept friend request');
+ toast.error(err.response?.data?.message || 'Failed to accept friend request');
}
};
@@ -73,7 +74,7 @@ function Friends({ user, setUserChanged }) {
}
} catch (err) {
console.error('Error rejecting friend request:', err);
- window.alert(err.response?.data?.message || 'Failed to reject friend request');
+ toast.error(err.response?.data?.message || 'Failed to reject friend request');
}
};
@@ -94,7 +95,7 @@ function Friends({ user, setUserChanged }) {
}
} catch (err) {
console.error('Error removing friend:', err);
- window.alert(err.response?.data?.message || 'Failed to remove friend');
+ toast.error(err.response?.data?.message || 'Failed to remove friend');
}
};
diff --git a/src/app/pages/Profile.jsx b/src/app/pages/Profile.jsx
index 51f4c99..0754d2c 100644
--- a/src/app/pages/Profile.jsx
+++ b/src/app/pages/Profile.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate, useParams } from 'react-router-dom';
+import { toast } from 'react-toastify';
import PageMeta from '../components/ui/PageMeta';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, KeyboardSensor } from '@dnd-kit/core';
import { arrayMove, SortableContext, rectSortingStrategy, useSortable, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
@@ -79,7 +80,7 @@ function Profile({ user: currentUser, setUserChanged }) {
}
} catch (err) {
console.error('Error updating visibility:', err);
- window.alert('Failed to update visibility');
+ toast.error('Failed to update visibility');
} finally {
setIsUpdatingVisibility(false);
}
@@ -146,11 +147,11 @@ function Profile({ user: currentUser, setUserChanged }) {
}
if (response && !response.data.success) {
- window.alert(response.data.message || 'Action failed');
+ toast.error(response.data.message || 'Action failed');
}
} catch (err) {
console.error(`Error ${action}ing friend:`, err);
- window.alert(err.response?.data?.message || `Failed to ${action} friend`);
+ toast.error(err.response?.data?.message || `Failed to ${action} friend`);
} finally {
setIsUpdatingFriendship(false);
}
diff --git a/src/app/pages/ShowMediaDetails.jsx b/src/app/pages/ShowMediaDetails.jsx
index fb35094..fa6b10f 100644
--- a/src/app/pages/ShowMediaDetails.jsx
+++ b/src/app/pages/ShowMediaDetails.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { Link, useParams, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
+import { toast } from 'react-toastify';
import PageMeta from '../components/ui/PageMeta';
import DeleteModal from "../components/modals/DeleteModal";
import TagMaker from "../components/TagMaker";
@@ -93,7 +94,15 @@ function ShowMediaDetails({
setLoaded(true);
}
})
- .catch(() => {});
+ .catch((err) => {
+ const status = err?.response?.status;
+ if (status === 404) {
+ navigate('/404');
+ return;
+ }
+ toast.error(err?.response?.data?.message || 'Could not load media.');
+ setLoaded(true);
+ });
}
}
}, [loaded, mediaType, group, navigate, mediaList, dataSource, onGetMediaById, propMediaList]);
@@ -161,6 +170,7 @@ function ShowMediaDetails({
})
.catch((err) => {
setTempMedia(media);
+ toast.error(err?.response?.data?.message || 'Failed to save changes.');
});
};
@@ -189,7 +199,9 @@ function ShowMediaDetails({
const finalUrl = currentSearch ? `${backUrl}${currentSearch}` : backUrl;
navigate(finalUrl);
})
- .catch(() => {});
+ .catch((err) => {
+ toast.error(err?.response?.data?.message || 'Failed to delete.');
+ });
}
};
@@ -228,7 +240,7 @@ function ShowMediaDetails({
setDuplicateId(created.ID);
setShowDuplicateModal(true);
} else {
- window.alert('Failed to duplicate media');
+ toast.error('Failed to duplicate media');
}
return;
}
@@ -240,7 +252,7 @@ function ShowMediaDetails({
setShowDuplicateModal(true);
})
.catch((err) => {
- window.alert('Failed to duplicate media');
+ toast.error('Failed to duplicate media');
});
}
diff --git a/src/app/pages/ShowMediaDetails.test.jsx b/src/app/pages/ShowMediaDetails.test.jsx
index 9fe9682..f415158 100644
--- a/src/app/pages/ShowMediaDetails.test.jsx
+++ b/src/app/pages/ShowMediaDetails.test.jsx
@@ -4,8 +4,16 @@ import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import axios from 'axios';
import ShowMediaDetails from './ShowMediaDetails';
+import { toast } from 'react-toastify';
jest.mock('axios');
+jest.mock('react-toastify', () => ({
+ toast: {
+ error: jest.fn(),
+ success: jest.fn(),
+ },
+ ToastContainer: () => null,
+}));
// DeleteModal manages its own show/hide via internal state.
// Use require inside the factory (variables must be prefixed with 'mock' to be allowed by Jest).
@@ -79,7 +87,7 @@ beforeEach(() => {
axios.get.mockResolvedValue({ data: mockMedia });
axios.put.mockResolvedValue({ data: mockMedia });
axios.delete.mockResolvedValue({ data: { toDo: false } });
- window.alert = jest.fn();
+ toast.error.mockClear();
});
describe('ShowMediaDetails', () => {
diff --git a/src/app/pages/ShowMediaList.jsx b/src/app/pages/ShowMediaList.jsx
index b944b73..dfbea22 100644
--- a/src/app/pages/ShowMediaList.jsx
+++ b/src/app/pages/ShowMediaList.jsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
+import { toast } from 'react-toastify';
import { Link, useParams, useNavigate, useLocation } from 'react-router-dom';
import PageMeta from '../components/ui/PageMeta';
@@ -248,7 +249,7 @@ function ShowMediaList({
navigate('/');
})
.catch(err => {
- window.alert('Error deleting media type');
+ toast.error('Error deleting media type');
console.error(err);
});
}
@@ -285,7 +286,9 @@ function ShowMediaList({
if (dataSource === 'demo' && onMoveToTier) {
onMoveToTier(activeId, targetTier, updatedTargetTier.length - 1);
} else if (dataSource === 'api') {
- axios.put(constants['SERVER_URL'] + `/api/media/${mediaType}/${activeId}`, { tier: targetTier }).catch(() => {});
+ axios
+ .put(constants['SERVER_URL'] + `/api/media/${mediaType}/${activeId}`, { tier: targetTier })
+ .catch(() => toast.error('Failed to save changes. Please try again.'));
}
}
return;
@@ -316,7 +319,9 @@ function ShowMediaList({
if (dataSource === 'demo' && onReorderInTier) {
onReorderInTier(sourceTier, toDoState, updatedList.map(m => m.ID));
} else if (dataSource === 'api') {
- axios.put(constants['SERVER_URL'] + `/api/media/${mediaType}/${toDoString}/${sourceTier}/reorder`, { orderedIds: updatedList.map(m => m.ID) }).catch(() => {});
+ axios
+ .put(constants['SERVER_URL'] + `/api/media/${mediaType}/${toDoString}/${sourceTier}/reorder`, { orderedIds: updatedList.map(m => m.ID) })
+ .catch(() => toast.error('Failed to save order. Please try again.'));
}
} else {
const fromList = [...(localByTier[sourceTier] || [])];
@@ -331,7 +336,9 @@ function ShowMediaList({
if (dataSource === 'demo' && onMoveToTier) {
onMoveToTier(activeId, destTier, destIndex);
} else if (dataSource === 'api') {
- axios.put(constants['SERVER_URL'] + `/api/media/${mediaType}/${activeId}`, { tier: destTier, orderIndex: destIndex }).catch(() => {});
+ axios
+ .put(constants['SERVER_URL'] + `/api/media/${mediaType}/${activeId}`, { tier: destTier, orderIndex: destIndex })
+ .catch(() => toast.error('Failed to save changes. Please try again.'));
}
}
};
diff --git a/src/index.js b/src/index.js
index cd7961b..f373bc6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
import './styling/index.css';
import './styling/App.css';
import "@fortawesome/fontawesome-free/css/all.min.css";
+import { ToastContainer } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
@@ -11,6 +13,7 @@ const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+
);