Skip to content

Commit

Permalink
feat: add toast notifications for experiment save success and error h…
Browse files Browse the repository at this point in the history
…andling
  • Loading branch information
drikusroor committed Jan 7, 2025
1 parent 255ebe8 commit 471ae3d
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 38 deletions.
76 changes: 40 additions & 36 deletions backend/experiment/templates/form/experiment-form/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ExperimentForm from './components/ExperimentForm';
import Login from './components/Login';
import { useState } from 'react';
import { FiLogOut } from 'react-icons/fi';
import { Toasts } from './components/Toasts';

function App() {
const [isCollapsed, setIsCollapsed] = useState(false);
Expand Down Expand Up @@ -41,47 +42,50 @@ function App() {
}

return (
<Router>
<div className="flex relative">
<nav className={`fixed h-screen bg-white shadow-lg transition ${isCollapsed ? 'w-16' : 'w-40 xl:w-48'}`}>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="w-full p-4 text-left hover:bg-gray-100"
>
{isCollapsed ? '→' : '←'}
</button>
<div className={`flex flex-col gap-5 transition ${isCollapsed ? 'p-2' : 'p-4'}`}>
<Link to="/experiments"
className={`transition text-blue-500 hover:bg-gray-100 rounded flex items-center gap-2`}
title="Experiments">
<AiTwotoneExperiment className="block min-w-5 h-5" />
<div className="flex-shrink-1">
{!isCollapsed && 'Experiments'}
</div>
</Link>
<>
<Router>
<div className="flex relative">
<nav className={`fixed h-screen bg-white shadow-lg transition ${isCollapsed ? 'w-16' : 'w-40 xl:w-48'}`}>
<button
onClick={handleLogout}
className="transition text-red-500 hover:bg-gray-100 rounded flex items-center gap-2"
onClick={() => setIsCollapsed(!isCollapsed)}
className="w-full p-4 text-left hover:bg-gray-100"
>
<FiLogOut className="block min-w-5 h-5" />
<div className="flex-shrink-1">
{!isCollapsed && 'Logout'}
</div>
{isCollapsed ? '→' : '←'}
</button>
<div className={`flex flex-col gap-5 transition ${isCollapsed ? 'p-2' : 'p-4'}`}>
<Link to="/experiments"
className={`transition text-blue-500 hover:bg-gray-100 rounded flex items-center gap-2`}
title="Experiments">
<AiTwotoneExperiment className="block min-w-5 h-5" />
<div className="flex-shrink-1">
{!isCollapsed && 'Experiments'}
</div>
</Link>
<button
onClick={handleLogout}
className="transition text-red-500 hover:bg-gray-100 rounded flex items-center gap-2"
>
<FiLogOut className="block min-w-5 h-5" />
<div className="flex-shrink-1">
{!isCollapsed && 'Logout'}
</div>
</button>
</div>
</nav>
<div className={`absolute right-0 bg-gray-100 min-h-screen p-4 transition max-w-full ${isCollapsed ? 'left-16' : 'left-40 xl:left-48'}`}>
<h1 className="text-4xl font-bold mb-8">
MUSCLE forms
</h1>
<Routes>
<Route path="/experiments" element={<ExperimentsOverview />} />
<Route path="/experiments/:id/*" element={<ExperimentForm />} />
<Route path="/" element={<ExperimentsOverview />} />
</Routes>
</div>
</nav>
<div className={`absolute right-0 bg-gray-100 min-h-screen p-4 transition max-w-full ${isCollapsed ? 'left-16' : 'left-40 xl:left-48'}`}>
<h1 className="text-4xl font-bold mb-8">
MUSCLE forms
</h1>
<Routes>
<Route path="/experiments" element={<ExperimentsOverview />} />
<Route path="/experiments/:id/*" element={<ExperimentForm />} />
<Route path="/" element={<ExperimentsOverview />} />
</Routes>
</div>
</div>
</Router>
</Router>
<Toasts />
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ExperimentForm: React.FC<ExperimentFormProps> = () => {
const experiment = useBoundStore(state => state.experiment);
const setExperiment = useBoundStore(state => state.setExperiment);
const patchExperiment = useBoundStore(state => state.patchExperiment);
const addToast = useBoundStore(state => state.addToast);

const [success, setSuccess] = useState(false);
const [activeTab, setActiveTab] = useState<'translatedContent' | 'phases'>('translatedContent');
Expand Down Expand Up @@ -88,8 +89,18 @@ const ExperimentForm: React.FC<ExperimentFormProps> = () => {
const savedExperiment = await saveExperiment(experiment);
setSuccess(true);
setExperiment(savedExperiment);
addToast({
message: "Experiment saved successfully!",
duration: 3000,
level: "info"
});
} catch (err) {
console.error(err);
addToast({
message: "Failed to save experiment. Please try again.",
duration: 5000,
level: "error"
});
console.error("Error submitting form:", err);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Toast as ToastType } from '../utils/store';

interface ToastProps {
toast: ToastType;
}

export const Toast: React.FC<ToastProps> = ({ toast }) => {
const bgColorClass = {
info: 'bg-gray-800',
warning: 'bg-yellow-600',
error: 'bg-red-600'
}[toast.level];

return (
<div className={`fixed bottom-4 right-4 ${bgColorClass} text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-300 ease-in-out`}>
<p>{toast.message}</p>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useBoundStore } from '../utils/store';
import { Toast } from './Toast';

export const Toasts: React.FC = () => {
const toasts = useBoundStore((state) => state.toasts);

return (
<div className="fixed bottom-4 right-4 flex flex-col gap-2">
{toasts.map((toast, index) => (
<Toast key={index} toast={toast} />
))}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,33 @@ const createExperimentSlice: StateCreator<ExperimentSlice> = (set) => ({
})
});

export const useBoundStore = create<ExperimentSlice>((...args) => ({
interface Toast {
message: string;
duration: number;
level: "info" | "warning" | "error";
}

interface ToastsSlice {
toasts: Toast[];
addToast: (toast: Toast) => void;
}

const createToastsSlice: StateCreator<ToastsSlice> = (set) => ({
toasts: [],
addToast: (toast) => {
// Add toast to the list of toasts, then, based on the toast's duration, remove it after a certain amount of time
set((state) => ({ toasts: [...state.toasts, toast] }));
setTimeout(() => {
set((state) => ({ toasts: state.toasts.filter((t) => t !== toast) }));
}, toast.duration);
},
});

export const useBoundStore = create<
ExperimentSlice & ToastsSlice
>((...args) => ({
...createExperimentSlice(...args),
...createToastsSlice(...args),
}));

export default useBoundStore;

0 comments on commit 471ae3d

Please sign in to comment.