Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions src/components/NotificationsDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const NotificationsDropdown: React.FC = () => {
const { notifications, unreadNotifications, refreshUserData, friends } = useAuth();
const navigate = useNavigate();
const [localNotifications, setLocalNotifications] = React.useState<Notification[]>(notifications);
const [dismissedIds, setDismissedIds] = React.useState<Set<string>>(new Set());

React.useEffect(() => {
setLocalNotifications(notifications);
Expand All @@ -54,7 +53,10 @@ const NotificationsDropdown: React.FC = () => {
const result = await acceptFriendRequest(auth.currentUser?.uid || '', senderId);
if (result) {
toast.success('Friend request accepted');
setDismissedIds(prev => new Set(prev).add(notificationId));
await markNotificationAsRead(auth.currentUser?.uid || '', notificationId);
setLocalNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
);
refreshUserData();
} else {
toast.error('Failed to accept friend request');
Expand All @@ -65,7 +67,10 @@ const NotificationsDropdown: React.FC = () => {
const result = await declineFriendRequest(auth.currentUser?.uid || '', senderId);
if (result) {
toast.success('Friend request declined');
setDismissedIds(prev => new Set(prev).add(notificationId));
await markNotificationAsRead(auth.currentUser?.uid || '', notificationId);
setLocalNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
);
refreshUserData();
} else {
toast.error('Failed to decline friend request');
Expand Down Expand Up @@ -132,7 +137,6 @@ const NotificationsDropdown: React.FC = () => {
{localNotifications && localNotifications.length > 0 ? (
<DropdownMenuGroup>
{localNotifications
.filter(n => !dismissedIds.has(n.id))
.sort((a, b) => {
let aTime: number;
let bTime: number;
Expand All @@ -159,7 +163,7 @@ const NotificationsDropdown: React.FC = () => {
.map((notification, index) => (
<React.Fragment key={notification.id || index}>
{notification.type === 'friendRequest' && notification.senderInfo ? (
<div className={`px-2 py-2 hover:bg-accent flex items-center justify-between ${!notification.read ? 'bg-accent/40' : ''}`}>
<div className={`px-2 py-2 hover:bg-accent flex items-center justify-between ${!notification.read ? 'bg-accent/40' : 'opacity-60'}`}>
<div className="flex items-center">
{notification.senderInfo && (
<Avatar className="h-8 w-8 mr-2">
Expand All @@ -175,24 +179,26 @@ const NotificationsDropdown: React.FC = () => {
</p>
</div>
</div>
<div className="flex space-x-1">
<Button
size="sm"
variant="outline"
onClick={() => handleAcceptFriendRequest(notification.senderInfo!.id, notification.id)}
className="h-7 w-7 p-0"
>
<Check className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeclineFriendRequest(notification.senderInfo!.id, notification.id)}
className="h-7 w-7 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
{!notification.read && (
<div className="flex space-x-1">
<Button
size="sm"
variant="outline"
onClick={() => handleAcceptFriendRequest(notification.senderInfo!.id, notification.id)}
className="h-7 w-7 p-0"
>
<Check className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeclineFriendRequest(notification.senderInfo!.id, notification.id)}
className="h-7 w-7 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
)}
</div>
) : (
<DropdownMenuItem
Expand Down
178 changes: 150 additions & 28 deletions src/components/RelapseCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@

import React, { useState, useEffect } from 'react';
import { Calendar } from '@/components/ui/calendar';
import { getRelapseCalendarData, getRelapseData } from '../utils/firebase';
import { getRelapseCalendarData, getRelapseData, updateCalendarDay } from '../utils/firebase';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Card } from '@/components/ui/card';
import { isSameDay, differenceInDays } from 'date-fns';
import { motion } from 'framer-motion';
import { DayContentProps } from 'react-day-picker';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { toast } from 'sonner';

interface RelapseCalendarProps {
userId?: string;
showStats?: boolean;
editable?: boolean;
}

interface DayInfo {
Expand All @@ -22,42 +35,87 @@ interface DayInfo {
} | null;
}

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

useEffect(() => {
const fetchData = async () => {
if (!userId) {
setIsLoading(false);
return;
}
// Edit dialog state
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [selectedDayData, setSelectedDayData] = useState<DayInfo | null>(null);
const [editMarkAsRelapse, setEditMarkAsRelapse] = useState(false);
const [editTriggers, setEditTriggers] = useState('');
const [editNotes, setEditNotes] = useState('');
const [isSaving, setIsSaving] = useState(false);

try {
setIsLoading(true);
// Get calendar visualization data
const data = await getRelapseCalendarData(userId);
setCalendarData(data);

// Get analytics data for accurate stats
const relapseData = await getRelapseData(userId, 'all');
setStats({
cleanDays: relapseData.cleanDays,
relapseDays: relapseData.relapseDays,
netGrowth: relapseData.netGrowth
});
} catch (error) {
console.error('Error fetching calendar data:', error);
} finally {
setIsLoading(false);
}
};
const fetchData = async () => {
if (!userId) {
setIsLoading(false);
return;
}

try {
setIsLoading(true);
// Get calendar visualization data
const data = await getRelapseCalendarData(userId);
setCalendarData(data);

// Get analytics data for accurate stats
const relapseData = await getRelapseData(userId, 'all');
setStats({
cleanDays: relapseData.cleanDays,
relapseDays: relapseData.relapseDays,
netGrowth: relapseData.netGrowth
});
} catch (error) {
console.error('Error fetching calendar data:', error);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
fetchData();
}, [userId]);

const handleDayClick = (day: Date) => {
if (!editable || !userId) return;
const dayData = calendarData.find(d => isSameDay(d.date, day)) || null;
setSelectedDay(day);
setSelectedDayData(dayData);
setEditMarkAsRelapse(dayData?.hadRelapse ?? false);
setEditTriggers(dayData?.relapseInfo?.triggers ?? '');
setEditNotes(dayData?.relapseInfo?.notes ?? '');
setEditDialogOpen(true);
};

const handleSaveEdit = async () => {
if (!userId || !selectedDay) return;
setIsSaving(true);
try {
const success = await updateCalendarDay(
userId,
selectedDay,
editMarkAsRelapse,
editMarkAsRelapse ? editTriggers : undefined,
editMarkAsRelapse ? editNotes : undefined
);
if (success) {
toast.success(editMarkAsRelapse ? 'Day marked as relapse' : 'Day marked as clean');
setEditDialogOpen(false);
await fetchData();
} else {
toast.error('Failed to update day');
}
} catch (error) {
toast.error('Failed to update day');
} finally {
setIsSaving(false);
}
};

// Custom day rendering with dots for relapse status
const renderDay = (props: DayContentProps) => {
const day = props.date;
Expand All @@ -72,7 +130,10 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="relative w-full h-full flex items-center justify-center">
<div
className={`relative w-full h-full flex items-center justify-center ${editable ? 'cursor-pointer' : ''}`}
onClick={editable ? () => handleDayClick(day) : undefined}
>
<div className="w-7 h-7 flex items-center justify-center">
{day.getDate()}
</div>
Expand All @@ -93,6 +154,7 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
) : (
<p className="text-green-500">Clean day</p>
)}
{editable && <p className="text-xs text-muted-foreground mt-1">Click to edit</p>}
</div>
</TooltipContent>
</Tooltip>
Expand Down Expand Up @@ -120,6 +182,9 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
</div>
) : (
<Card className="p-4 w-full">
{editable && (
<p className="text-xs text-muted-foreground mb-2 text-center">Click any day to mark it as clean or relapse</p>
)}
<Calendar
mode="default"
month={month}
Expand Down Expand Up @@ -155,6 +220,63 @@ const RelapseCalendar: React.FC<RelapseCalendarProps> = ({ userId, showStats = f
</div>
</Card>
)}

{/* Edit day dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Edit Day – {selectedDay ? new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric', year: 'numeric' }).format(selectedDay) : ''}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="flex gap-3">
<Button
variant={editMarkAsRelapse ? 'outline' : 'default'}
className={!editMarkAsRelapse ? 'bg-green-600 hover:bg-green-700 text-white' : ''}
onClick={() => setEditMarkAsRelapse(false)}
>
Clean Day
</Button>
<Button
variant={editMarkAsRelapse ? 'destructive' : 'outline'}
onClick={() => setEditMarkAsRelapse(true)}
>
Relapse Day
</Button>
</div>

{editMarkAsRelapse && (
<>
<div className="space-y-1">
<Label htmlFor="edit-triggers">Triggers</Label>
<Input
id="edit-triggers"
placeholder="What triggered this?"
value={editTriggers}
onChange={e => setEditTriggers(e.target.value)}
/>
</div>
<div className="space-y-1">
<Label htmlFor="edit-notes">Notes (optional)</Label>
<Textarea
id="edit-notes"
placeholder="Any additional notes..."
value={editNotes}
onChange={e => setEditNotes(e.target.value)}
/>
</div>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSaveEdit} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ const Analytics: React.FC = () => {
</CardHeader>
<CardContent>
<div className="space-y-6">
<RelapseCalendar userId={currentUser?.uid} />
<RelapseCalendar userId={currentUser?.uid} editable={true} />

<div className="grid grid-cols-3 gap-4 mt-6">
<div className="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg text-center">
Expand Down
37 changes: 37 additions & 0 deletions src/utils/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,43 @@ export const getAllUsernames = async (): Promise<UserProfile[]> => {
}
};

export const updateCalendarDay = async (
userId: string,
date: Date,
markAsRelapse: boolean,
triggers?: string,
notes?: string
): Promise<boolean> => {
try {
const userDocRef = doc(db, 'users', userId);
const userDoc = await getDoc(userDocRef);

if (!userDoc.exists()) return false;

const userData = userDoc.data();
const relapses: Relapse[] = userData.relapses || [];
const targetDay = startOfDay(date);

// Remove any existing relapse entry for this day
const filtered = relapses.filter(
r => !isSameDay(startOfDay(r.timestamp.toDate()), targetDay)
);

if (markAsRelapse) {
// Add a new relapse entry for this day at noon to avoid timezone edge cases
const relapseDate = new Date(targetDay);
relapseDate.setHours(12, 0, 0, 0);
filtered.push({
timestamp: Timestamp.fromDate(relapseDate),
triggers: triggers || '',
notes: notes || ''
});
}

await updateDoc(userDocRef, { relapses: filtered });
return true;
} catch (error) {
console.error('Error updating calendar day:', error);
export const adminUpdateUser = async (userId: string, updates: { role?: 'admin' | 'member'; streakDays?: number }): Promise<boolean> => {
try {
const userRef = doc(db, 'users', userId);
Expand Down
Loading