| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -44,7 +44,6 @@ const NotificationsDropdown: React.FC = () => { | |||
| 44 | 44 | const { notifications, unreadNotifications, refreshUserData, friends } = useAuth(); | |
| 45 | 45 | const navigate = useNavigate(); | |
| 46 | 46 | const [localNotifications, setLocalNotifications] = React.useState<Notification[]>(notifications); | |
| 47 | - const [dismissedIds, setDismissedIds] = React.useState<Set<string>>(new Set()); | ||
| 48 | 47 | ||
| 49 | 48 | React.useEffect(() => { | |
| 50 | 49 | setLocalNotifications(notifications); | |
@@ -54,7 +53,10 @@ const NotificationsDropdown: React.FC = () => { | |||
| 54 | 53 | const result = await acceptFriendRequest(auth.currentUser?.uid || '', senderId); | |
| 55 | 54 | if (result) { | |
| 56 | 55 | 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 | + ); | ||
| 58 | 60 | refreshUserData(); | |
| 59 | 61 | } else { | |
| 60 | 62 | toast.error('Failed to accept friend request'); | |
@@ -65,7 +67,10 @@ const NotificationsDropdown: React.FC = () => { | |||
| 65 | 67 | const result = await declineFriendRequest(auth.currentUser?.uid || '', senderId); | |
| 66 | 68 | if (result) { | |
| 67 | 69 | 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 | + ); | ||
| 69 | 74 | refreshUserData(); | |
| 70 | 75 | } else { | |
| 71 | 76 | toast.error('Failed to decline friend request'); | |
@@ -132,7 +137,6 @@ const NotificationsDropdown: React.FC = () => { | |||
| 132 | 137 | {localNotifications && localNotifications.length > 0 ? ( | |
| 133 | 138 | <DropdownMenuGroup> | |
| 134 | 139 | {localNotifications | |
| 135 | - .filter(n => !dismissedIds.has(n.id)) | ||
| 136 | 140 | .sort((a, b) => { | |
| 137 | 141 | let aTime: number; | |
| 138 | 142 | let bTime: number; | |
@@ -159,7 +163,7 @@ const NotificationsDropdown: React.FC = () => { | |||
| 159 | 163 | .map((notification, index) => ( | |
| 160 | 164 | <React.Fragment key={notification.id || index}> | |
| 161 | 165 | {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'}`}> | ||
| 163 | 167 | <div className="flex items-center"> | |
| 164 | 168 | {notification.senderInfo && ( | |
| 165 | 169 | <Avatar className="h-8 w-8 mr-2"> | |
@@ -175,24 +179,26 @@ const NotificationsDropdown: React.FC = () => { | |||
| 175 | 179 | </p> | |
| 176 | 180 | </div> | |
| 177 | 181 | </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 | + )} | ||
| 196 | 202 | </div> | |
| 197 | 203 | ) : ( | |
| 198 | 204 | <DropdownMenuItem | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,16 +1,29 @@ | |||
| 1 | 1 | ||
| 2 | 2 | import React, { useState, useEffect } from 'react'; | |
| 3 | 3 | import { Calendar } from '@/components/ui/calendar'; | |
| 4 | - import { getRelapseCalendarData, getRelapseData } from '../utils/firebase'; | ||
| 4 | + import { getRelapseCalendarData, getRelapseData, updateCalendarDay } from '../utils/firebase'; | ||
| 5 | 5 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; | |
| 6 | 6 | import { Card } from '@/components/ui/card'; | |
| 7 | 7 | import { isSameDay, differenceInDays } from 'date-fns'; | |
| 8 | 8 | import { motion } from 'framer-motion'; | |
| 9 | 9 | 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'; | ||
| 10 | 22 | ||
| 11 | 23 | interface RelapseCalendarProps { | |
| 12 | 24 | userId?: string; | |
| 13 | 25 | showStats?: boolean; | |
| 26 | + editable?: boolean; | ||
| 14 | 27 | } | |
| 15 | 28 | ||
| 16 | 29 | interface DayInfo { | |
@@ -22,42 +35,87 @@ interface DayInfo { | |||
| 22 | 35 | } | null; | |
| 23 | 36 | } | |
| 24 | 37 | ||
| 25 | - const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = false }) => { | ||
| 38 | + const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = false, editable = false }) => { | ||
| 26 | 39 | const [calendarData, setCalendarData] = useState<DayInfo[]>([]); | |
| 27 | 40 | const [isLoading, setIsLoading] = useState(true); | |
| 28 | 41 | const [month, setMonth] = useState<Date>(new Date()); | |
| 29 | 42 | const [stats, setStats] = useState({ cleanDays: 0, relapseDays: 0, netGrowth: 0 }); | |
| 30 | 43 | ||
| 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); | ||
| 37 | 52 | ||
| 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 | + }; | ||
| 57 | 78 | ||
| 79 | + useEffect(() => { | ||
| 58 | 80 | fetchData(); | |
| 59 | 81 | }, [userId]); | |
| 60 | 82 | ||
| 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 | + | ||
| 61 | 119 | // Custom day rendering with dots for relapse status | |
| 62 | 120 | const renderDay = (props: DayContentProps) => { | |
| 63 | 121 | const day = props.date; | |
@@ -72,7 +130,10 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f | |||
| 72 | 130 | return ( | |
| 73 | 131 | <Tooltip> | |
| 74 | 132 | <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 | + > | ||
| 76 | 137 | <div className="w-7 h-7 flex items-center justify-center"> | |
| 77 | 138 | {day.getDate()} | |
| 78 | 139 | </div> | |
@@ -93,6 +154,7 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f | |||
| 93 | 154 | ) : ( | |
| 94 | 155 | <p className="text-green-500">Clean day</p> | |
| 95 | 156 | )} | |
| 157 | + {editable && <p className="text-xs text-muted-foreground mt-1">Click to edit</p>} | ||
| 96 | 158 | </div> | |
| 97 | 159 | </TooltipContent> | |
| 98 | 160 | </Tooltip> | |
@@ -120,6 +182,9 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f | |||
| 120 | 182 | </div> | |
| 121 | 183 | ) : ( | |
| 122 | 184 | <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 | + )} | ||
| 123 | 188 | <Calendar | |
| 124 | 189 | mode="default" | |
| 125 | 190 | month={month} | |
@@ -155,6 +220,63 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f | |||
| 155 | 220 | </div> | |
| 156 | 221 | </Card> | |
| 157 | 222 | )} | |
| 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> | ||
| 158 | 280 | </div> | |
| 159 | 281 | </TooltipProvider> | |
| 160 | 282 | ); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -306,7 +306,7 @@ const Analytics: React.FC = () => { | |||
| 306 | 306 | </CardHeader> | |
| 307 | 307 | <CardContent> | |
| 308 | 308 | <div className="space-y-6"> | |
| 309 | - <RelapseCalendar userId={currentUser?.uid} /> | ||
| 309 | + <RelapseCalendar userId={currentUser?.uid} editable={true} /> | ||
| 310 | 310 | ||
| 311 | 311 | <div className="grid grid-cols-3 gap-4 mt-6"> | |
| 312 | 312 | <div className="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg text-center"> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1236,6 +1236,43 @@ export const getAllUsernames = async (): Promise<UserProfile[]> => { | |||
| 1236 | 1236 | } | |
| 1237 | 1237 | }; | |
| 1238 | 1238 | ||
| 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); | ||
| 1239 | 1276 | export const adminUpdateUser = async (userId: string, updates: { role?: 'admin' | 'member'; streakDays?: number }): Promise<boolean> => { | |
| 1240 | 1277 | try { | |
| 1241 | 1278 | const userRef = doc(db, 'users', userId); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments