Skip to content

Commit 268587f

Browse files
authored
Merge pull request #50 from Debugging-Disciples/copilot/edit-calendar-dates-notifications
Add editable recovery calendar dates and fix notification persistence on friend request actions
2 parents ea00bb8 + ad253a9 commit 268587f

4 files changed

Lines changed: 217 additions & 52 deletions

File tree

src/components/NotificationsDropdown.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const NotificationsDropdown: React.FC = () => {
4444
const { notifications, unreadNotifications, refreshUserData, friends } = useAuth();
4545
const navigate = useNavigate();
4646
const [localNotifications, setLocalNotifications] = React.useState<Notification[]>(notifications);
47-
const [dismissedIds, setDismissedIds] = React.useState<Set<string>>(new Set());
4847

4948
React.useEffect(() => {
5049
setLocalNotifications(notifications);
@@ -54,7 +53,10 @@ const NotificationsDropdown: React.FC = () => {
5453
const result = await acceptFriendRequest(auth.currentUser?.uid || '', senderId);
5554
if (result) {
5655
toast.success('Friend request accepted');
57-
setDismissedIds(prev => new Set(prev).add(notificationId));
56+
await markNotificationAsRead(auth.currentUser?.uid || '', notificationId);
57+
setLocalNotifications(prev =>
58+
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
59+
);
5860
refreshUserData();
5961
} else {
6062
toast.error('Failed to accept friend request');
@@ -65,7 +67,10 @@ const NotificationsDropdown: React.FC = () => {
6567
const result = await declineFriendRequest(auth.currentUser?.uid || '', senderId);
6668
if (result) {
6769
toast.success('Friend request declined');
68-
setDismissedIds(prev => new Set(prev).add(notificationId));
70+
await markNotificationAsRead(auth.currentUser?.uid || '', notificationId);
71+
setLocalNotifications(prev =>
72+
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
73+
);
6974
refreshUserData();
7075
} else {
7176
toast.error('Failed to decline friend request');
@@ -132,7 +137,6 @@ const NotificationsDropdown: React.FC = () => {
132137
{localNotifications && localNotifications.length > 0 ? (
133138
<DropdownMenuGroup>
134139
{localNotifications
135-
.filter(n => !dismissedIds.has(n.id))
136140
.sort((a, b) => {
137141
let aTime: number;
138142
let bTime: number;
@@ -159,7 +163,7 @@ const NotificationsDropdown: React.FC = () => {
159163
.map((notification, index) => (
160164
<React.Fragment key={notification.id || index}>
161165
{notification.type === 'friendRequest' && notification.senderInfo ? (
162-
<div className={`px-2 py-2 hover:bg-accent flex items-center justify-between ${!notification.read ? 'bg-accent/40' : ''}`}>
166+
<div className={`px-2 py-2 hover:bg-accent flex items-center justify-between ${!notification.read ? 'bg-accent/40' : 'opacity-60'}`}>
163167
<div className="flex items-center">
164168
{notification.senderInfo && (
165169
<Avatar className="h-8 w-8 mr-2">
@@ -175,24 +179,26 @@ const NotificationsDropdown: React.FC = () => {
175179
</p>
176180
</div>
177181
</div>
178-
<div className="flex space-x-1">
179-
<Button
180-
size="sm"
181-
variant="outline"
182-
onClick={() => handleAcceptFriendRequest(notification.senderInfo!.id, notification.id)}
183-
className="h-7 w-7 p-0"
184-
>
185-
<Check className="h-4 w-4" />
186-
</Button>
187-
<Button
188-
size="sm"
189-
variant="outline"
190-
onClick={() => handleDeclineFriendRequest(notification.senderInfo!.id, notification.id)}
191-
className="h-7 w-7 p-0"
192-
>
193-
<X className="h-4 w-4" />
194-
</Button>
195-
</div>
182+
{!notification.read && (
183+
<div className="flex space-x-1">
184+
<Button
185+
size="sm"
186+
variant="outline"
187+
onClick={() => handleAcceptFriendRequest(notification.senderInfo!.id, notification.id)}
188+
className="h-7 w-7 p-0"
189+
>
190+
<Check className="h-4 w-4" />
191+
</Button>
192+
<Button
193+
size="sm"
194+
variant="outline"
195+
onClick={() => handleDeclineFriendRequest(notification.senderInfo!.id, notification.id)}
196+
className="h-7 w-7 p-0"
197+
>
198+
<X className="h-4 w-4" />
199+
</Button>
200+
</div>
201+
)}
196202
</div>
197203
) : (
198204
<DropdownMenuItem

src/components/RelapseCalendar.tsx

Lines changed: 150 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11

22
import React, { useState, useEffect } from 'react';
33
import { Calendar } from '@/components/ui/calendar';
4-
import { getRelapseCalendarData, getRelapseData } from '../utils/firebase';
4+
import { getRelapseCalendarData, getRelapseData, updateCalendarDay } from '../utils/firebase';
55
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
66
import { Card } from '@/components/ui/card';
77
import { isSameDay, differenceInDays } from 'date-fns';
88
import { motion } from 'framer-motion';
99
import { DayContentProps } from 'react-day-picker';
10+
import {
11+
Dialog,
12+
DialogContent,
13+
DialogHeader,
14+
DialogTitle,
15+
DialogFooter,
16+
} from '@/components/ui/dialog';
17+
import { Button } from '@/components/ui/button';
18+
import { Label } from '@/components/ui/label';
19+
import { Input } from '@/components/ui/input';
20+
import { Textarea } from '@/components/ui/textarea';
21+
import { toast } from 'sonner';
1022

1123
interface RelapseCalendarProps {
1224
userId?: string;
1325
showStats?: boolean;
26+
editable?: boolean;
1427
}
1528

1629
interface DayInfo {
@@ -22,42 +35,87 @@ interface DayInfo {
2235
} | null;
2336
}
2437

25-
const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = false }) => {
38+
const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = false, editable = false }) => {
2639
const [calendarData, setCalendarData] = useState<DayInfo[]>([]);
2740
const [isLoading, setIsLoading] = useState(true);
2841
const [month, setMonth] = useState<Date>(new Date());
2942
const [stats, setStats] = useState({ cleanDays: 0, relapseDays: 0, netGrowth: 0 });
3043

31-
useEffect(() => {
32-
const fetchData = async () => {
33-
if (!userId) {
34-
setIsLoading(false);
35-
return;
36-
}
44+
// Edit dialog state
45+
const [editDialogOpen, setEditDialogOpen] = useState(false);
46+
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
47+
const [selectedDayData, setSelectedDayData] = useState<DayInfo | null>(null);
48+
const [editMarkAsRelapse, setEditMarkAsRelapse] = useState(false);
49+
const [editTriggers, setEditTriggers] = useState('');
50+
const [editNotes, setEditNotes] = useState('');
51+
const [isSaving, setIsSaving] = useState(false);
3752

38-
try {
39-
setIsLoading(true);
40-
// Get calendar visualization data
41-
const data = await getRelapseCalendarData(userId);
42-
setCalendarData(data);
43-
44-
// Get analytics data for accurate stats
45-
const relapseData = await getRelapseData(userId, 'all');
46-
setStats({
47-
cleanDays: relapseData.cleanDays,
48-
relapseDays: relapseData.relapseDays,
49-
netGrowth: relapseData.netGrowth
50-
});
51-
} catch (error) {
52-
console.error('Error fetching calendar data:', error);
53-
} finally {
54-
setIsLoading(false);
55-
}
56-
};
53+
const fetchData = async () => {
54+
if (!userId) {
55+
setIsLoading(false);
56+
return;
57+
}
58+
59+
try {
60+
setIsLoading(true);
61+
// Get calendar visualization data
62+
const data = await getRelapseCalendarData(userId);
63+
setCalendarData(data);
64+
65+
// Get analytics data for accurate stats
66+
const relapseData = await getRelapseData(userId, 'all');
67+
setStats({
68+
cleanDays: relapseData.cleanDays,
69+
relapseDays: relapseData.relapseDays,
70+
netGrowth: relapseData.netGrowth
71+
});
72+
} catch (error) {
73+
console.error('Error fetching calendar data:', error);
74+
} finally {
75+
setIsLoading(false);
76+
}
77+
};
5778

79+
useEffect(() => {
5880
fetchData();
5981
}, [userId]);
6082

83+
const handleDayClick = (day: Date) => {
84+
if (!editable || !userId) return;
85+
const dayData = calendarData.find(d => isSameDay(d.date, day)) || null;
86+
setSelectedDay(day);
87+
setSelectedDayData(dayData);
88+
setEditMarkAsRelapse(dayData?.hadRelapse ?? false);
89+
setEditTriggers(dayData?.relapseInfo?.triggers ?? '');
90+
setEditNotes(dayData?.relapseInfo?.notes ?? '');
91+
setEditDialogOpen(true);
92+
};
93+
94+
const handleSaveEdit = async () => {
95+
if (!userId || !selectedDay) return;
96+
setIsSaving(true);
97+
try {
98+
const success = await updateCalendarDay(
99+
userId,
100+
selectedDay,
101+
editMarkAsRelapse,
102+
editMarkAsRelapse ? editTriggers : undefined,
103+
editMarkAsRelapse ? editNotes : undefined
104+
);
105+
if (success) {
106+
toast.success(editMarkAsRelapse ? 'Day marked as relapse' : 'Day marked as clean');
107+
setEditDialogOpen(false);
108+
await fetchData();
109+
} else {
110+
toast.error('Failed to update day');
111+
}
112+
} catch (error) {
113+
toast.error('Failed to update day');
114+
} finally {
115+
setIsSaving(false);
116+
}
117+
};
118+
61119
// Custom day rendering with dots for relapse status
62120
const renderDay = (props: DayContentProps) => {
63121
const day = props.date;
@@ -72,7 +130,10 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
72130
return (
73131
<Tooltip>
74132
<TooltipTrigger asChild>
75-
<div className="relative w-full h-full flex items-center justify-center">
133+
<div
134+
className={`relative w-full h-full flex items-center justify-center ${editable ? 'cursor-pointer' : ''}`}
135+
onClick={editable ? () => handleDayClick(day) : undefined}
136+
>
76137
<div className="w-7 h-7 flex items-center justify-center">
77138
{day.getDate()}
78139
</div>
@@ -93,6 +154,7 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
93154
) : (
94155
<p className="text-green-500">Clean day</p>
95156
)}
157+
{editable && <p className="text-xs text-muted-foreground mt-1">Click to edit</p>}
96158
</div>
97159
</TooltipContent>
98160
</Tooltip>
@@ -120,6 +182,9 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
120182
</div>
121183
) : (
122184
<Card className="p-4 w-full">
185+
{editable && (
186+
<p className="text-xs text-muted-foreground mb-2 text-center">Click any day to mark it as clean or relapse</p>
187+
)}
123188
<Calendar
124189
mode="default"
125190
month={month}
@@ -155,6 +220,63 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
155220
</div>
156221
</Card>
157222
)}
223+
224+
{/* Edit day dialog */}
225+
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
226+
<DialogContent>
227+
<DialogHeader>
228+
<DialogTitle>
229+
Edit Day – {selectedDay ? new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric', year: 'numeric' }).format(selectedDay) : ''}
230+
</DialogTitle>
231+
</DialogHeader>
232+
<div className="space-y-4 py-2">
233+
<div className="flex gap-3">
234+
<Button
235+
variant={editMarkAsRelapse ? 'outline' : 'default'}
236+
className={!editMarkAsRelapse ? 'bg-green-600 hover:bg-green-700 text-white' : ''}
237+
onClick={() => setEditMarkAsRelapse(false)}
238+
>
239+
Clean Day
240+
</Button>
241+
<Button
242+
variant={editMarkAsRelapse ? 'destructive' : 'outline'}
243+
onClick={() => setEditMarkAsRelapse(true)}
244+
>
245+
Relapse Day
246+
</Button>
247+
</div>
248+
249+
{editMarkAsRelapse && (
250+
<>
251+
<div className="space-y-1">
252+
<Label htmlFor="edit-triggers">Triggers</Label>
253+
<Input
254+
id="edit-triggers"
255+
placeholder="What triggered this?"
256+
value={editTriggers}
257+
onChange={e => setEditTriggers(e.target.value)}
258+
/>
259+
</div>
260+
<div className="space-y-1">
261+
<Label htmlFor="edit-notes">Notes (optional)</Label>
262+
<Textarea
263+
id="edit-notes"
264+
placeholder="Any additional notes..."
265+
value={editNotes}
266+
onChange={e => setEditNotes(e.target.value)}
267+
/>
268+
</div>
269+
</>
270+
)}
271+
</div>
272+
<DialogFooter>
273+
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>Cancel</Button>
274+
<Button onClick={handleSaveEdit} disabled={isSaving}>
275+
{isSaving ? 'Saving...' : 'Save'}
276+
</Button>
277+
</DialogFooter>
278+
</DialogContent>
279+
</Dialog>
158280
</div>
159281
</TooltipProvider>
160282
);

src/pages/Analytics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ const Analytics: React.FC = () => {
306306
</CardHeader>
307307
<CardContent>
308308
<div className="space-y-6">
309-
<RelapseCalendar userId={currentUser?.uid} />
309+
<RelapseCalendar userId={currentUser?.uid} editable={true} />
310310

311311
<div className="grid grid-cols-3 gap-4 mt-6">
312312
<div className="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg text-center">

src/utils/firebase.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,43 @@ export const getAllUsernames = async (): Promise<UserProfile[]> => {
12361236
}
12371237
};
12381238

1239+
export const updateCalendarDay = async (
1240+
userId: string,
1241+
date: Date,
1242+
markAsRelapse: boolean,
1243+
triggers?: string,
1244+
notes?: string
1245+
): Promise<boolean> => {
1246+
try {
1247+
const userDocRef = doc(db, 'users', userId);
1248+
const userDoc = await getDoc(userDocRef);
1249+
1250+
if (!userDoc.exists()) return false;
1251+
1252+
const userData = userDoc.data();
1253+
const relapses: Relapse[] = userData.relapses || [];
1254+
const targetDay = startOfDay(date);
1255+
1256+
// Remove any existing relapse entry for this day
1257+
const filtered = relapses.filter(
1258+
r => !isSameDay(startOfDay(r.timestamp.toDate()), targetDay)
1259+
);
1260+
1261+
if (markAsRelapse) {
1262+
// Add a new relapse entry for this day at noon to avoid timezone edge cases
1263+
const relapseDate = new Date(targetDay);
1264+
relapseDate.setHours(12, 0, 0, 0);
1265+
filtered.push({
1266+
timestamp: Timestamp.fromDate(relapseDate),
1267+
triggers: triggers || '',
1268+
notes: notes || ''
1269+
});
1270+
}
1271+
1272+
await updateDoc(userDocRef, { relapses: filtered });
1273+
return true;
1274+
} catch (error) {
1275+
console.error('Error updating calendar day:', error);
12391276
export const adminUpdateUser = async (userId: string, updates: { role?: 'admin' | 'member'; streakDays?: number }): Promise<boolean> => {
12401277
try {
12411278
const userRef = doc(db, 'users', userId);

0 commit comments

Comments
 (0)