diff --git a/frontend/src/App.css b/frontend/src/App.css index 162009a..d661175 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -265,4 +265,50 @@ body { .input-group input:focus { border-color: var(--accent-color); background-color: #252525; +} + +/* Toast Notification */ +.toast-container { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + pointer-events: none; +} + +.toast { + background-color: var(--bg-topbar); + color: var(--text-primary); + padding: 0.8rem 1.5rem; + border-radius: 8px; + font-size: 0.9rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border: 1px solid var(--border-color); + opacity: 0; + transform: translateY(10px); + animation: toast-fade-in-out ease forwards; +} + +.toast.success { + border-left: 4px solid #2e7d32; +} + +@keyframes toast-fade-in-out { + 0% { + opacity: 0; + transform: translateY(10px); + } + 10% { + opacity: 1; + transform: translateY(0); + } + 90% { + opacity: 1; + transform: translateY(0); + } + 100% { + opacity: 0; + transform: translateY(-10px); + } } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 579add8..cdfcfcb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,8 @@ interface ExifData { iso: string; } +const TOAST_DURATION_MS = 3000; + function App() { const canvasRef = useRef(null); const [imageLoaded, setImageLoaded] = useState(false); @@ -30,6 +32,42 @@ function App() { const [filePath, setFilePath] = useState(""); const [isMac, setIsMac] = useState(false); const [sourceMimeType, setSourceMimeType] = useState(""); + const [toastMessage, setToastMessage] = useState(null); + const toastTimerRef = useRef(null); + const toastRafRef = useRef(null); + + const showToast = (message: string) => { + if (toastTimerRef.current !== null) { + window.clearTimeout(toastTimerRef.current); + toastTimerRef.current = null; + } + if (toastRafRef.current !== null) { + window.cancelAnimationFrame(toastRafRef.current); + toastRafRef.current = null; + } + + // Force a re-render by clearing the state first + setToastMessage(null); + toastRafRef.current = requestAnimationFrame(() => { + setToastMessage(message); + toastTimerRef.current = window.setTimeout(() => { + setToastMessage(null); + toastTimerRef.current = null; + }, TOAST_DURATION_MS); + toastRafRef.current = null; + }); + }; + + useEffect(() => { + return () => { + if (toastTimerRef.current !== null) { + window.clearTimeout(toastTimerRef.current); + } + if (toastRafRef.current !== null) { + window.cancelAnimationFrame(toastRafRef.current); + } + }; + }, []); useEffect(() => { Environment().then(env => { @@ -224,6 +262,8 @@ function App() { const errText = await response.text(); console.error("Save failed:", errText); alert("Failed to save image: " + errText); + } else { + showToast("Image saved successfully"); } } catch (err) { console.error("Failed to execute SaveImage:", err); @@ -338,6 +378,12 @@ function App() { )} + + {toastMessage && ( +
+
{toastMessage}
+
+ )} );