Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
46 changes: 46 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface ExifData {
iso: string;
}

const TOAST_DURATION_MS = 3000;

function App() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [imageLoaded, setImageLoaded] = useState(false);
Expand All @@ -30,6 +32,42 @@ function App() {
const [filePath, setFilePath] = useState("");
const [isMac, setIsMac] = useState(false);
const [sourceMimeType, setSourceMimeType] = useState("");
const [toastMessage, setToastMessage] = useState<string | null>(null);
const toastTimerRef = useRef<number | null>(null);
const toastRafRef = useRef<number | null>(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
Comment on lines +44 to +49
setToastMessage(null);
Comment on lines +38 to +50
toastRafRef.current = requestAnimationFrame(() => {
setToastMessage(message);
toastTimerRef.current = window.setTimeout(() => {
setToastMessage(null);
toastTimerRef.current = null;
}, TOAST_DURATION_MS);
toastRafRef.current = null;
});
Comment on lines +39 to +58
};
Comment on lines +39 to +59
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useEffect(() => {
Comment on lines +38 to 72
return () => {
if (toastTimerRef.current !== null) {
window.clearTimeout(toastTimerRef.current);
}
if (toastRafRef.current !== null) {
window.cancelAnimationFrame(toastRafRef.current);
}
};
}, []);

useEffect(() => {
Environment().then(env => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -338,6 +378,12 @@ function App() {
</div>
</aside>
)}

{toastMessage && (
<div className="toast-container" aria-live="polite" aria-atomic="true" role="status">
<div className="toast success" style={{ animationDuration: `${TOAST_DURATION_MS}ms` }}>{toastMessage}</div>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)}
</main>
</div>
);
Expand Down