Skip to content

Commit dfa7863

Browse files
authored
Merge branch 'main' into main
2 parents 9d86140 + 0d6d620 commit dfa7863

11 files changed

Lines changed: 486 additions & 37 deletions

File tree

package.json

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
"@testing-library/dom": "^10.4.1",
77
"@testing-library/react": "^16.3.0",
88
"@testing-library/user-event": "^13.5.0",
9-
"date-fns": "^4.1.0",
109
"framer-motion": "^12.23.22",
1110
"lucide-react": "^0.544.0",
1211
"react": "^19.2.0",
1312
"react-dom": "^19.2.0",
1413
"react-scripts": "5.0.1",
1514
"react-swipeable": "^7.0.2",
16-
"web-vitals": "^2.1.4"
15+
"web-vitals": "^2.1.4",
16+
"date-fns": "^4.1.0"
1717
},
1818
"scripts": {
1919
"start": "react-scripts start",
2020
"build": "react-scripts build",
21-
"test": "jest",
21+
"test": "react-scripts test",
22+
"test:coverage": "react-scripts test --coverage",
2223
"eject": "react-scripts eject"
2324
},
2425
"eslintConfig": {
@@ -39,13 +40,14 @@
3940
"last 1 safari version"
4041
]
4142
},
42-
"devDependencies": {
43-
"@babel/core": "^7.28.4",
44-
"@babel/preset-env": "^7.28.3",
45-
"@babel/preset-react": "^7.27.1",
46-
"@testing-library/jest-dom": "^6.9.1",
47-
"identity-obj-proxy": "^3.0.0",
48-
"jest": "^27.5.1",
49-
"jsdom": "^27.0.0"
43+
"jest": {
44+
"collectCoverageFrom": [
45+
"src/hooks/useAchievements.js"
46+
],
47+
"testMatch": [
48+
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
49+
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}",
50+
"<rootDir>/tests/**/*.{spec,test}.{js,jsx,ts,tsx}"
51+
]
5052
}
5153
}

src/App.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,51 @@ input[type="text"]:focus, select:focus {
289289
transform: rotate(360deg);
290290
}
291291
}
292+
293+
/* --- Styles for Motivational Quote Component --- */
294+
295+
.motivational-quote-container {
296+
width: 100%;
297+
max-width: 600px; /* Adjust max-width as needed */
298+
margin: 0 auto 1.5rem auto; /* Centers the component and adds bottom margin */
299+
padding: 1.5rem;
300+
background-color: #f8f9fa; /* A light, clean background */
301+
border: 1px solid #dee2e6;
302+
border-radius: 8px;
303+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
304+
text-align: center;
305+
}
306+
307+
.quote-header {
308+
display: flex;
309+
align-items: center;
310+
justify-content: center;
311+
gap: 0.5rem; /* Space between icon and title */
312+
}
313+
314+
.quote-icon {
315+
color: #f59e0b; /* A warm yellow for the lightbulb */
316+
}
317+
318+
.quote-title {
319+
font-size: 1.1rem;
320+
font-weight: 600;
321+
color: #343a40;
322+
margin: 0;
323+
}
324+
325+
.quote-text {
326+
font-size: 1.25rem;
327+
font-style: italic;
328+
color: #495057;
329+
margin: 1rem 0;
330+
}
331+
332+
.quote-author {
333+
display: block; /* Makes it take its own line */
334+
text-align: right; /* Aligns the author to the right */
335+
font-size: 0.9rem;
336+
font-weight: 500;
337+
color: #6c757d;
338+
margin-top: 0.5rem;
339+
}

src/App.js

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import './App.css';
1+
import './App.css'; // This line imports the styles you just added
22
import { useState, useCallback } from 'react';
3+
// Import custom hook for habit reminders
4+
import { useReminder } from './hooks/useReminder';
35
import { subWeeks, addWeeks, subMonths, addMonths } from 'date-fns';
46
import { useHabits } from './hooks/useHabits';
57
import { achievements } from './constants';
@@ -10,6 +12,9 @@ import CalendarToggle from './components/CalendarToggle';
1012
import HabitGrid from './components/HabitGrid';
1113
import HabitCard from './components/HabitCard';
1214
import AchievementModal from './components/AchievementModal';
15+
import NotificationSettings from './components/NotificationSettings/NotificationSettings';
16+
// --- 1. IMPORT THE NEW MOTIVATIONAL QUOTE COMPONENT ---
17+
import MotivationalQuote from './components/MotivationalQuote';
1318

1419
function App() {
1520
const {
@@ -24,10 +29,18 @@ function App() {
2429

2530
const [selectedCategory, setSelectedCategory] = useState('All');
2631
const [calendarMode, setCalendarMode] = useState('90day');
27-
const [newlyUnlocked, setNewlyUnlocked] = useState([]);
28-
const [modalOpen, setModalOpen] = useState(false);
32+
// Removed unused state: newlyUnlocked, setNewlyUnlocked, modalOpen, setModalOpen
2933
const [selectedHabit, setSelectedHabit] = useState(null);
3034
const [currentDate, setCurrentDate] = useState(new Date());
35+
// User opt-in for habit reminders (permission control)
36+
const [reminderPermission, setReminderPermission] = useState(false);
37+
// Function to show a simple toast/alert for reminders
38+
const showReminder = (habit) => {
39+
alert(`Reminder: Complete Daily ${habit.name}!`);
40+
};
41+
42+
// Integrate reminder hook: only active if user has opted in
43+
useReminder(habits, showReminder, reminderPermission);
3144

3245
const filteredHabits = selectedCategory === 'All'
3346
? habits
@@ -43,16 +56,15 @@ function App() {
4356

4457
const handleToggleCompletion = useCallback((habitId, day) => {
4558
toggleCompletion(habitId, day);
46-
// Note: Handling newly unlocked achievements would need to be adjusted
4759
}, [toggleCompletion]);
4860

61+
// Show achievements modal for selected habit
4962
const handleViewAchievements = useCallback((habit) => {
5063
setSelectedHabit(habit);
51-
setModalOpen(true);
5264
}, []);
5365

66+
// Close achievements modal
5467
const handleCloseModal = useCallback(() => {
55-
setModalOpen(false);
5668
setSelectedHabit(null);
5769
}, []);
5870

@@ -90,21 +102,15 @@ function App() {
90102
link.click();
91103
document.body.removeChild(link);
92104
};
93-
94-
// START: Added JSON Export Logic
105+
95106
const handleExportJSON = () => {
96107
if (habits.length === 0) {
97108
alert("There are no habits to export.");
98109
return;
99110
}
100111

101-
// Convert the habits array to a pretty-printed JSON string
102112
const jsonContent = JSON.stringify(habits, null, 2);
103-
104-
// Create a Blob from the JSON string
105113
const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
106-
107-
// Create a link element to trigger the download
108114
const link = document.createElement('a');
109115
const url = URL.createObjectURL(blob);
110116
link.setAttribute('href', url);
@@ -114,14 +120,27 @@ function App() {
114120
link.click();
115121
document.body.removeChild(link);
116122
};
117-
// END: Added JSON Export Logic
118123

119124
return (
120125
<div className="App">
126+
{/* Reminder permission opt-in UI */}
127+
<div className="mb-4">
128+
{/* Checkbox to allow user to enable/disable reminders */}
129+
<label className="flex items-center gap-2">
130+
<input
131+
type="checkbox"
132+
checked={reminderPermission}
133+
onChange={e => setReminderPermission(e.target.checked)}
134+
/>
135+
Enable habit reminders (opt-in)
136+
</label>
137+
</div>
121138
<header className="App-header">
122139
<HabitForm onAddHabit={addHabit} />
140+
141+
{/* --- 2. RENDER THE NEW COMPONENT RIGHT HERE --- */}
142+
<MotivationalQuote />
123143

124-
{/* START: Added Export Buttons Container */}
125144
<div className="flex gap-2 mb-4">
126145
<button
127146
onClick={handleExportCSV}
@@ -136,9 +155,9 @@ function App() {
136155
Export to JSON
137156
</button>
138157
</div>
158+
<NotificationSettings />
139159
{/* END: Added Export Buttons Container */}
140160

141-
{/* Achievements display */}
142161
<div className="mb-4 w-full max-w-sm text-left">
143162
<h4 className="font-semibold mb-2">Achievements:</h4>
144163
{habits.map(habit => (
@@ -167,7 +186,6 @@ function App() {
167186
</div>
168187

169188
<CategoryFilter selectedCategory={selectedCategory} onSelectCategory={setSelectedCategory} />
170-
171189
<CalendarToggle calendarMode={calendarMode} onToggle={toggleCalendarMode} />
172190

173191
{calendarMode !== '90day' && (
@@ -177,7 +195,6 @@ function App() {
177195
</div>
178196
)}
179197

180-
{/* Display current habits */}
181198
<div style={{ marginTop: '20px', textAlign: 'left' }}>
182199
<h3>Current Habits:</h3>
183200
{filteredHabits.length === 0 ? (
@@ -192,7 +209,7 @@ function App() {
192209
averageCompletion={averageCompletion}
193210
onViewAchievements={handleViewAchievements}
194211
onDelete={deleteHabit}
195-
newlyUnlocked={newlyUnlocked}
212+
// newlyUnlocked prop removed
196213
/>
197214
))}
198215
</div>
@@ -223,4 +240,4 @@ function App() {
223240
);
224241
}
225242

226-
export default App;
243+
export default App;

src/components/HabitCard.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { motion } from 'framer-motion';
22
import { Award, Star, Medal, TrendingUp } from 'lucide-react';
33
import { achievements } from '../constants';
44

5-
function HabitCard({ habit, calculateStreak, averageCompletion, onViewAchievements, onDelete, newlyUnlocked }) {
5+
function HabitCard({ habit, calculateStreak, averageCompletion, onViewAchievements, onDelete }) {
66
const streak = calculateStreak(habit);
77
const avg = averageCompletion(habit);
88

@@ -14,11 +14,10 @@ function HabitCard({ habit, calculateStreak, averageCompletion, onViewAchievemen
1414
const ach = achievements.find(a => a.id === aid);
1515
const Icon = ach.icon === 'Award' ? Award : ach.icon === 'Star' ? Star : Medal;
1616
const bgColor = ach.icon === 'Award' ? 'bg-yellow-500' : ach.icon === 'Star' ? 'bg-blue-500' : 'bg-green-500';
17-
const isNew = newlyUnlocked.includes(aid);
17+
// No 'isNew' animation since newlyUnlocked is not used
1818
return (
1919
<motion.div
2020
key={aid}
21-
initial={isNew ? { scale: 0 } : { scale: 1 }}
2221
animate={{ scale: 1 }}
2322
whileHover={{ scale: 1.05 }}
2423
transition={{ duration: 0.5 }}

src/components/HabitForm.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useState } from 'react';
2+
import PropTypes from 'prop-types';
23
import { categories } from '../constants';
34

45
function HabitForm({ onAddHabit }) {
5-
const [newHabit, setNewHabit] = useState({ name: '', category: 'General' });
6+
// State includes reminderTime for optional daily reminder
7+
const [newHabit, setNewHabit] = useState({ name: '', category: 'General', reminderTime: '' });
68

79
const handleSubmit = (e) => {
810
e.preventDefault();
@@ -17,6 +19,7 @@ function HabitForm({ onAddHabit }) {
1719

1820
return (
1921
<form onSubmit={handleSubmit} className="mb-4">
22+
{/* Habit name input */}
2023
<input
2124
type="text"
2225
placeholder="Habit name"
@@ -25,13 +28,22 @@ function HabitForm({ onAddHabit }) {
2528
className="mb-2 p-2 border border-gray-300 rounded-md w-full max-w-sm"
2629
required
2730
/>
31+
{/* Category selection */}
2832
<select
2933
value={newHabit.category}
3034
onChange={(e) => setNewHabit({ ...newHabit, category: e.target.value })}
3135
className="mb-2 p-2 border border-gray-300 rounded-md w-full max-w-sm"
3236
>
3337
{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
3438
</select>
39+
{/* Reminder time input (optional) */}
40+
<input
41+
type="time"
42+
value={newHabit.reminderTime}
43+
onChange={e => setNewHabit({ ...newHabit, reminderTime: e.target.value })}
44+
className="mb-2 p-2 border border-gray-300 rounded-md w-full max-w-sm"
45+
placeholder="Reminder time (optional)"
46+
/>
3547
<button
3648
type="submit"
3749
className="mb-4 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
@@ -42,4 +54,9 @@ function HabitForm({ onAddHabit }) {
4254
);
4355
}
4456

57+
58+
HabitForm.propTypes = {
59+
onAddHabit: PropTypes.func.isRequired,
60+
};
61+
4562
export default HabitForm;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useState, useEffect } from 'react';
2+
import { Lightbulb } from 'lucide-react';
3+
4+
// A self-contained list of quotes. This can be easily expanded.
5+
const quotes = [
6+
{ text: "The secret of getting ahead is getting started.", author: "Mark Twain" },
7+
{ text: "Consistency is more important than perfection.", author: "Unknown" },
8+
{ text: "Code is like humor. When you have to explain it, it’s bad.", author: "Cory House" },
9+
{ text: "The only way to do great work is to love what you do.", author: "Steve Jobs" },
10+
{ text: "Success is the sum of small efforts, repeated day in and day out.", author: "Robert Collier" },
11+
{ text: "Don't watch the clock; do what it does. Keep going.", author: "Sam Levenson" },
12+
{ text: "The best time to plant a tree was 20 years ago. The second best time is now.", author: "Chinese Proverb" }
13+
];
14+
15+
function MotivationalQuote() {
16+
const [quote, setQuote] = useState({ text: '', author: '' });
17+
18+
// This useEffect runs only once when the component is first mounted.
19+
useEffect(() => {
20+
const randomIndex = Math.floor(Math.random() * quotes.length);
21+
setQuote(quotes[randomIndex]);
22+
}, []); // Empty dependency array ensures it runs only once.
23+
24+
if (!quote.text) {
25+
return null; // Don't render anything until a quote is selected
26+
}
27+
28+
return (
29+
<div className="motivational-quote-container">
30+
<div className="quote-header">
31+
<Lightbulb size={20} className="quote-icon" />
32+
<h3 className="quote-title">Food for Thought</h3>
33+
</div>
34+
<p className="quote-text">"{quote.text}"</p>
35+
<span className="quote-author">- {quote.author}</span>
36+
</div>
37+
);
38+
}
39+
40+
export default MotivationalQuote;

0 commit comments

Comments
 (0)