Skip to content

Commit 1a16e27

Browse files
committed
Notification & Achievements
1 parent 32594d2 commit 1a16e27

File tree

13 files changed

+651
-22
lines changed

13 files changed

+651
-22
lines changed

src/App.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import React from 'react';
22
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
33
import { ThemeProvider } from './contexts/ThemeContext';
4+
import { NotificationProvider } from './contexts/NotificationContext';
45
import { Navigation } from './components/Navigation';
6+
import { NotificationContainer } from './components/notifications/NotificationContainer';
57
import { Dashboard } from './pages/Dashboard';
68
import { Habits } from './pages/Habits';
79
import { Progress } from './pages/Progress';
8-
import { Settings } from './pages/Settings';
910

1011
function App() {
1112
return (
1213
<ThemeProvider>
13-
<Router>
14-
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
15-
<Navigation />
16-
<Routes>
17-
<Route path="/" element={<Navigate to="/dashboard" replace />} />
18-
<Route path="/dashboard" element={<Dashboard />} />
19-
<Route path="/habits" element={<Habits />} />
20-
<Route path="/progress" element={<Progress />} />
21-
<Route path="/settings" element={<Settings />} />
22-
</Routes>
23-
</div>
24-
</Router>
14+
<NotificationProvider>
15+
<Router>
16+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
17+
<Navigation />
18+
<Routes>
19+
<Route path="/" element={<Navigate to="/dashboard" replace />} />
20+
<Route path="/dashboard" element={<Dashboard />} />
21+
<Route path="/habits" element={<Habits />} />
22+
<Route path="/progress" element={<Progress />} />
23+
</Routes>
24+
<NotificationContainer />
25+
</div>
26+
</Router>
27+
</NotificationProvider>
2528
</ThemeProvider>
2629
);
2730
}

src/components/AddHabitModal.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export const AddHabitModal: React.FC<AddHabitModalProps> = ({
3838
customDays: [] as number[],
3939
category: 'Health & Fitness',
4040
startDate: new Date().toISOString().split('T')[0],
41+
reminderEnabled: false,
42+
reminderTime: '09:00',
4143
});
4244

4345
const handleSubmit = (e: React.FormEvent) => {
@@ -51,6 +53,8 @@ export const AddHabitModal: React.FC<AddHabitModalProps> = ({
5153
customDays: formData.frequency === 'custom' ? formData.customDays : undefined,
5254
category: formData.category,
5355
startDate: formData.startDate,
56+
reminderEnabled: formData.reminderEnabled,
57+
reminderTime: formData.reminderEnabled ? formData.reminderTime : undefined,
5458
});
5559

5660
// Reset form
@@ -61,6 +65,8 @@ export const AddHabitModal: React.FC<AddHabitModalProps> = ({
6165
customDays: [],
6266
category: 'Health & Fitness',
6367
startDate: new Date().toISOString().split('T')[0],
68+
reminderEnabled: false,
69+
reminderTime: '09:00',
6470
});
6571

6672
onClose();
@@ -192,6 +198,30 @@ export const AddHabitModal: React.FC<AddHabitModalProps> = ({
192198
onChange={(e) => setFormData(prev => ({ ...prev, startDate: e.target.value }))}
193199
/>
194200

201+
<div className="space-y-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
202+
<div className="flex items-center space-x-2">
203+
<input
204+
type="checkbox"
205+
id="reminderEnabled"
206+
checked={formData.reminderEnabled}
207+
onChange={(e) => setFormData(prev => ({ ...prev, reminderEnabled: e.target.checked }))}
208+
className="rounded text-indigo-600 focus:ring-indigo-500"
209+
/>
210+
<label htmlFor="reminderEnabled" className="text-sm font-medium text-gray-700 dark:text-gray-300">
211+
Enable daily reminders
212+
</label>
213+
</div>
214+
215+
{formData.reminderEnabled && (
216+
<Input
217+
label="Reminder Time"
218+
type="time"
219+
value={formData.reminderTime}
220+
onChange={(e) => setFormData(prev => ({ ...prev, reminderTime: e.target.value }))}
221+
/>
222+
)}
223+
</div>
224+
195225
<div className="flex space-x-3 pt-4">
196226
<Button type="submit" className="flex-1">
197227
Add Habit

src/components/Navigation.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ export const Navigation: React.FC = () => {
99
const navItems = [
1010
{ to: '/dashboard', icon: Home, label: 'Dashboard' },
1111
{ to: '/habits', icon: CheckSquare, label: 'Habits' },
12-
{ to: '/progress', icon: BarChart3, label: 'Progress' },
13-
{ to: '/settings', icon: Settings, label: 'Settings' },
12+
{ to: '/progress', icon: BarChart3, label: 'Progress' }
1413
];
1514

1615
const toggleMobileMenu = () => {

src/components/ThemeToggle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const ThemeToggle: React.FC = () => {
1111
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-all duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
1212
aria-label="Toggle theme"
1313
>
14-
{theme === 'light' ? (
14+
{theme === 'dark' ? (
1515
<Moon className="w-5 h-5" />
1616
) : (
1717
<Sun className="w-5 h-5" />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { useNotifications } from '../../contexts/NotificationContext';
3+
import { NotificationToast } from './NotificationToast';
4+
5+
export const NotificationContainer: React.FC = () => {
6+
const { notifications } = useNotifications();
7+
8+
return (
9+
<div className="fixed top-4 right-4 z-50 space-y-2 max-w-sm w-full">
10+
{notifications.map((notification) => (
11+
<NotificationToast
12+
key={notification.id}
13+
notification={notification}
14+
/>
15+
))}
16+
</div>
17+
);
18+
};
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Trophy } from 'lucide-react';
3+
import { useNotifications } from '../../contexts/NotificationContext';
4+
import { Notification } from '../../types';
5+
6+
interface NotificationToastProps {
7+
notification: Notification;
8+
}
9+
10+
export const NotificationToast: React.FC<NotificationToastProps> = ({ notification }) => {
11+
const { removeNotification } = useNotifications();
12+
const [isVisible, setIsVisible] = useState(false);
13+
const [isLeaving, setIsLeaving] = useState(false);
14+
15+
useEffect(() => {
16+
// Trigger entrance animation
17+
const timer = setTimeout(() => setIsVisible(true), 10);
18+
return () => clearTimeout(timer);
19+
}, []);
20+
21+
const handleClose = () => {
22+
setIsLeaving(true);
23+
setTimeout(() => {
24+
removeNotification(notification.id);
25+
}, 300);
26+
};
27+
28+
const getIcon = () => {
29+
switch (notification.type) {
30+
case 'success':
31+
return <CheckCircle className="w-5 h-5 text-emerald-500" />;
32+
case 'error':
33+
return <AlertCircle className="w-5 h-5 text-red-500" />;
34+
case 'warning':
35+
return <AlertTriangle className="w-5 h-5 text-amber-500" />;
36+
case 'achievement':
37+
return <Trophy className="w-5 h-5 text-yellow-500" />;
38+
default:
39+
return <Info className="w-5 h-5 text-blue-500" />;
40+
}
41+
};
42+
43+
const getBackgroundColor = () => {
44+
switch (notification.type) {
45+
case 'success':
46+
return 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800';
47+
case 'error':
48+
return 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800';
49+
case 'warning':
50+
return 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800';
51+
case 'achievement':
52+
return 'bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 border-yellow-200 dark:border-yellow-800';
53+
default:
54+
return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800';
55+
}
56+
};
57+
58+
return (
59+
<div
60+
className={`
61+
transform transition-all duration-300 ease-in-out
62+
${isVisible && !isLeaving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}
63+
${getBackgroundColor()}
64+
border rounded-lg shadow-lg p-4 max-w-sm w-full
65+
backdrop-blur-sm
66+
`}
67+
>
68+
<div className="flex items-start space-x-3">
69+
<div className="flex-shrink-0 mt-0.5">
70+
{getIcon()}
71+
</div>
72+
73+
<div className="flex-1 min-w-0">
74+
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
75+
{notification.title}
76+
</h4>
77+
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1">
78+
{notification.message}
79+
</p>
80+
81+
{notification.action && (
82+
<button
83+
onClick={notification.action.onClick}
84+
className="mt-2 text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300 transition-colors"
85+
>
86+
{notification.action.label}
87+
</button>
88+
)}
89+
</div>
90+
91+
<button
92+
onClick={handleClose}
93+
className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
94+
>
95+
<X className="w-4 h-4" />
96+
</button>
97+
</div>
98+
</div>
99+
);
100+
};

src/contexts/NotificationContext.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { createContext, useContext, useState, useCallback } from 'react';
2+
import { Notification, NotificationContextType } from '../types';
3+
4+
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
5+
6+
export const useNotifications = () => {
7+
const context = useContext(NotificationContext);
8+
if (context === undefined) {
9+
throw new Error('useNotifications must be used within a NotificationProvider');
10+
}
11+
return context;
12+
};
13+
14+
interface NotificationProviderProps {
15+
children: React.ReactNode;
16+
}
17+
18+
export const NotificationProvider: React.FC<NotificationProviderProps> = ({ children }) => {
19+
const [notifications, setNotifications] = useState<Notification[]>([]);
20+
21+
const addNotification = useCallback((notification: Omit<Notification, 'id' | 'createdAt'>) => {
22+
const id = Date.now().toString() + Math.random().toString(36).substr(2, 9);
23+
const newNotification: Notification = {
24+
...notification,
25+
id,
26+
createdAt: Date.now(),
27+
};
28+
29+
setNotifications(prev => [...prev, newNotification]);
30+
31+
// Auto-remove notification after duration (default 5 seconds)
32+
const duration = notification.duration || 5000;
33+
if (duration > 0) {
34+
setTimeout(() => {
35+
removeNotification(id);
36+
}, duration);
37+
}
38+
}, []);
39+
40+
const removeNotification = useCallback((id: string) => {
41+
setNotifications(prev => prev.filter(notification => notification.id !== id));
42+
}, []);
43+
44+
const clearAllNotifications = useCallback(() => {
45+
setNotifications([]);
46+
}, []);
47+
48+
return (
49+
<NotificationContext.Provider value={{
50+
notifications,
51+
addNotification,
52+
removeNotification,
53+
clearAllNotifications,
54+
}}>
55+
{children}
56+
</NotificationContext.Provider>
57+
);
58+
};

0 commit comments

Comments
 (0)