Skip to content

Commit 058463f

Browse files
committed
feat(contact): add animated snackbar component for better user feedback
- Create new Snackbar component with Framer Motion animations - Replace inline status messages with modern snackbar notifications - Add success/error icons and auto-dismiss functionality - Implement smooth slide-in/out animations with scale effects - Add close button for manual dismissal - Improve user experience with better visual feedback - Support both success and error message types - Auto-dismiss after 5 seconds with configurable duration
1 parent 1a05105 commit 058463f

File tree

2 files changed

+145
-24
lines changed

2 files changed

+145
-24
lines changed

src/components/Contact.tsx

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { motion } from 'framer-motion';
22
import type React from 'react';
33
import { useState, useEffect } from 'react';
44
import emailjs from '@emailjs/browser';
5+
import Snackbar from './Snackbar';
56

67
const Contact: React.FC = () => {
78
const [formData, setFormData] = useState({
@@ -10,7 +11,11 @@ const Contact: React.FC = () => {
1011
message: '',
1112
});
1213
const [isSubmitting, setIsSubmitting] = useState(false);
13-
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
14+
const [snackbar, setSnackbar] = useState({
15+
isOpen: false,
16+
message: '',
17+
type: 'success' as 'success' | 'error',
18+
});
1419

1520
// Initialize EmailJS
1621
useEffect(() => {
@@ -26,19 +31,26 @@ const Contact: React.FC = () => {
2631

2732
// Basic validation
2833
if (!formData.name.trim() || !formData.email.trim() || !formData.message.trim()) {
29-
setSubmitStatus('error');
34+
setSnackbar({
35+
isOpen: true,
36+
message: 'Please fill in all fields.',
37+
type: 'error',
38+
});
3039
return;
3140
}
3241

3342
// Email validation
3443
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3544
if (!emailRegex.test(formData.email.trim())) {
36-
setSubmitStatus('error');
45+
setSnackbar({
46+
isOpen: true,
47+
message: 'Please enter a valid email address.',
48+
type: 'error',
49+
});
3750
return;
3851
}
3952

4053
setIsSubmitting(true);
41-
setSubmitStatus('idle');
4254

4355
try {
4456
// EmailJS template parameters
@@ -61,7 +73,11 @@ const Contact: React.FC = () => {
6173
console.log('EmailJS result:', result);
6274

6375
if (result.status === 200 || result.text === 'OK') {
64-
setSubmitStatus('success');
76+
setSnackbar({
77+
isOpen: true,
78+
message: 'Message sent successfully! I\'ll get back to you soon.',
79+
type: 'success',
80+
});
6581

6682
// Reset form
6783
setFormData({
@@ -71,11 +87,19 @@ const Contact: React.FC = () => {
7187
});
7288
} else {
7389
console.error('EmailJS returned non-success status:', result);
74-
setSubmitStatus('error');
90+
setSnackbar({
91+
isOpen: true,
92+
message: 'Failed to send message. Please try again.',
93+
type: 'error',
94+
});
7595
}
7696
} catch (error) {
7797
console.error('Error sending email:', error);
78-
setSubmitStatus('error');
98+
setSnackbar({
99+
isOpen: true,
100+
message: 'Network error. Please check your connection and try again.',
101+
type: 'error',
102+
});
79103

80104
// Log additional error details for debugging
81105
if (error instanceof Error) {
@@ -86,11 +110,6 @@ const Contact: React.FC = () => {
86110
}
87111
} finally {
88112
setIsSubmitting(false);
89-
90-
// Reset status after 5 seconds
91-
setTimeout(() => {
92-
setSubmitStatus('idle');
93-
}, 5000);
94113
}
95114
};
96115

@@ -212,18 +231,14 @@ const Contact: React.FC = () => {
212231
/>
213232
</div>
214233

215-
{/* Status Messages */}
216-
{submitStatus === 'success' && (
217-
<div className="p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
218-
✅ Perfect! Your message has been sent successfully. I'll get back to you as soon as possible!
219-
</div>
220-
)}
221-
222-
{submitStatus === 'error' && (
223-
<div className="p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
224-
❌ There was an error sending your message. Please check your internet connection and try again, or email me directly at davidsyagustin@gmail.com
225-
</div>
226-
)}
234+
{/* Snackbar Component */}
235+
<Snackbar
236+
isOpen={snackbar.isOpen}
237+
message={snackbar.message}
238+
type={snackbar.type}
239+
onClose={() => setSnackbar({ ...snackbar, isOpen: false })}
240+
duration={5000}
241+
/>
227242

228243
<button
229244
type="submit"

src/components/Snackbar.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { motion, AnimatePresence } from 'framer-motion';
2+
import type React from 'react';
3+
import { useEffect } from 'react';
4+
import { FaCheckCircle, FaExclamationCircle, FaTimes } from 'react-icons/fa';
5+
6+
interface SnackbarProps {
7+
isOpen: boolean;
8+
message: string;
9+
type: 'success' | 'error';
10+
onClose: () => void;
11+
duration?: number;
12+
}
13+
14+
const Snackbar: React.FC<SnackbarProps> = ({
15+
isOpen,
16+
message,
17+
type,
18+
onClose,
19+
duration = 5000,
20+
}) => {
21+
useEffect(() => {
22+
if (isOpen) {
23+
const timer = setTimeout(() => {
24+
onClose();
25+
}, duration);
26+
27+
return () => clearTimeout(timer);
28+
}
29+
}, [isOpen, duration, onClose]);
30+
31+
const getIcon = () => {
32+
switch (type) {
33+
case 'success':
34+
return <FaCheckCircle className="text-green-500" />;
35+
case 'error':
36+
return <FaExclamationCircle className="text-red-500" />;
37+
default:
38+
return null;
39+
}
40+
};
41+
42+
const getStyles = () => {
43+
switch (type) {
44+
case 'success':
45+
return {
46+
bg: 'bg-green-50',
47+
border: 'border-green-200',
48+
text: 'text-green-800',
49+
icon: 'text-green-500',
50+
};
51+
case 'error':
52+
return {
53+
bg: 'bg-red-50',
54+
border: 'border-red-200',
55+
text: 'text-red-800',
56+
icon: 'text-red-500',
57+
};
58+
default:
59+
return {
60+
bg: 'bg-gray-50',
61+
border: 'border-gray-200',
62+
text: 'text-gray-800',
63+
icon: 'text-gray-500',
64+
};
65+
}
66+
};
67+
68+
const styles = getStyles();
69+
70+
return (
71+
<AnimatePresence>
72+
{isOpen && (
73+
<motion.div
74+
initial={{ opacity: 0, y: 50, scale: 0.3 }}
75+
animate={{ opacity: 1, y: 0, scale: 1 }}
76+
exit={{ opacity: 0, y: 50, scale: 0.3 }}
77+
transition={{
78+
duration: 0.3,
79+
ease: [0.4, 0, 0.2, 1],
80+
}}
81+
className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50"
82+
>
83+
<div
84+
className={`flex items-center gap-3 px-6 py-4 rounded-lg shadow-lg border ${styles.bg} ${styles.border} ${styles.text} max-w-md mx-4`}
85+
>
86+
<div className="flex-shrink-0 text-xl">
87+
{getIcon()}
88+
</div>
89+
<div className="flex-1 text-sm font-medium">
90+
{message}
91+
</div>
92+
<button
93+
onClick={onClose}
94+
className="flex-shrink-0 p-1 rounded-full hover:bg-black/10 transition-colors"
95+
aria-label="Close notification"
96+
>
97+
<FaTimes className="text-lg" />
98+
</button>
99+
</div>
100+
</motion.div>
101+
)}
102+
</AnimatePresence>
103+
);
104+
};
105+
106+
export default Snackbar;

0 commit comments

Comments
 (0)