Skip to content

Commit 37e0547

Browse files
authored
Merge pull request #20 from Awnder/feature/edit-streak-date
feat: add ability to edit streak start date
2 parents ec115da + 3d72e63 commit 37e0547

File tree

3 files changed

+320
-35
lines changed

3 files changed

+320
-35
lines changed

src/components/ui/date-picker.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import * as React from "react";
2+
import { ChevronLeft, ChevronRight } from "lucide-react";
3+
import { motion } from "framer-motion";
4+
5+
import { cn } from "@/lib/utils";
6+
7+
interface DatePickerProps extends React.HTMLAttributes<HTMLDivElement> {
8+
preselectedDate?: Date;
9+
onDateChange?: (date: Date) => void;
10+
}
11+
12+
const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
13+
({ className, preselectedDate, onDateChange, ...props }, ref) => {
14+
const [currentDate, setCurrentDate] = React.useState(new Date());
15+
const [selectedDate, setSelectedDate] = React.useState<Date>(preselectedDate || new Date());
16+
17+
const daysInMonth = (month: number, year: number) => {
18+
return new Date(year, month + 1, 0).getDate();
19+
};
20+
21+
const startDayOfMonth = (month: number, year: number) => {
22+
return new Date(year, month, 1).getDay();
23+
};
24+
25+
const handlePrevMonth = () => {
26+
setCurrentDate(
27+
new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)
28+
);
29+
};
30+
31+
const handleNextMonth = () => {
32+
setCurrentDate(
33+
new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)
34+
);
35+
};
36+
37+
const handleDateClick = (day: number) => {
38+
const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
39+
setSelectedDate(newDate);
40+
if (onDateChange) {
41+
onDateChange(newDate);
42+
}
43+
};
44+
45+
const renderDays = () => {
46+
const days = [];
47+
const daysInCurrentMonth = daysInMonth(
48+
currentDate.getMonth(),
49+
currentDate.getFullYear()
50+
);
51+
const startDay = startDayOfMonth(
52+
currentDate.getMonth(),
53+
currentDate.getFullYear()
54+
);
55+
56+
for (let i = 0; i < startDay; i++) {
57+
days.push(<div key={`empty-${i}`} />);
58+
}
59+
60+
for (let day = 1; day <= daysInCurrentMonth; day++) {
61+
days.push(
62+
<motion.div
63+
initial={{ opacity: 0 }}
64+
animate={{ opacity: 1 }}
65+
transition={{ delay: 0.01 * day }}
66+
key={day}
67+
className={cn(
68+
"grid-item place-self-center cursor-pointer rounded-lg w-[30px] h-[30px] flex items-center justify-center hover:bg-primary/30 transition-colors",
69+
{
70+
"border-0 text-primary-foreground bg-primary/80 hover:bg-primary/80":
71+
selectedDate?.getDate() === day &&
72+
selectedDate?.getMonth() === currentDate.getMonth() &&
73+
selectedDate?.getFullYear() === currentDate.getFullYear(),
74+
}, {
75+
"hover:border-0 bg-primary/20":
76+
new Date().getDate() === day &&
77+
new Date().getMonth() === currentDate.getMonth() &&
78+
new Date().getFullYear() === currentDate.getFullYear()
79+
}
80+
)}
81+
onClick={() => handleDateClick(day)}
82+
>
83+
{day}
84+
</motion.div>
85+
);
86+
}
87+
88+
return days;
89+
};
90+
91+
return (
92+
<div
93+
ref={ref}
94+
className={cn(
95+
"flex flex-col items-center max-h-[300px] w-full sm:w-lg p-1 border-2 rounded-md text-xs",
96+
className
97+
)}
98+
{...props}
99+
>
100+
<div className="flex flex-row items-center gap-2 mb-2">
101+
<motion.div
102+
initial={{ opacity: 0, x: -10 }}
103+
animate={{ opacity: 1, x: 0 }}
104+
transition={{ delay: 0.01 }}
105+
>
106+
<ChevronLeft
107+
onClick={handlePrevMonth}
108+
className="cursor-pointer rounded-md border-2 hover:bg-primary/30 hover:border-0 transition-colors"
109+
/>
110+
</motion.div>
111+
<motion.span
112+
initial={{ opacity: 0, y: -10 }}
113+
animate={{ opacity: 1, y: 0 }}
114+
transition={{ delay: 0.01 }}
115+
className="flex text-lg font-bold text-center"
116+
>
117+
{currentDate.toLocaleString("default", { month: "long" })}{" "}
118+
{currentDate.getFullYear()}
119+
</motion.span>
120+
<motion.div
121+
initial={{ opacity: 0, x: 10 }}
122+
animate={{ opacity: 1, x: 0 }}
123+
transition={{ delay: 0.01 }}
124+
>
125+
<ChevronRight
126+
onClick={handleNextMonth}
127+
className="cursor-pointer rounded-md border-2 hover:bg-primary/30 hover:border-0 transition-colors"
128+
/>
129+
</motion.div>
130+
</div>
131+
<div className="h-full w-full grid grid-cols-7 gap-1 p-2">
132+
{["S", "M", "T", "W", "T", "F", "S"].map((date, index) => (
133+
<motion.div
134+
initial={{ opacity: 0 }}
135+
animate={{ opacity: 1 }}
136+
transition={{ delay: 0.01 * index }}
137+
className="grid-item place-self-center"
138+
key={`${date}-${index}`}
139+
>
140+
{date}
141+
</motion.div>
142+
))}
143+
{renderDays()}
144+
</div>
145+
</div>
146+
);
147+
}
148+
);
149+
150+
DatePicker.displayName = "DatePicker";
151+
152+
export default DatePicker;

src/pages/Dashboard.tsx

Lines changed: 135 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
import React, { useState, useEffect } from 'react';
33
import { Link } from 'react-router-dom';
44
import { useAuth } from '../utils/auth';
5-
import { updateStreak, getUserProfile } from '../utils/firebase';
5+
import { updateStreak, getUserProfile, updateStreakStart } from '../utils/firebase';
66
import { cn } from '@/lib/utils';
77
import {
88
ArrowRight,
99
Calendar,
1010
Trophy,
1111
TrendingUp,
1212
MessageCircle,
13+
Map,
14+
HeartPulse,
15+
CheckIcon
1316
HeartPulse,
1417
BookOpen
1518
} from 'lucide-react';
@@ -22,6 +25,7 @@ import CommunityMap from '@/components/CommunityMap';
2225
import { motion } from 'framer-motion';
2326
import { toast } from 'sonner';
2427
import VerseSlideshow from '@/components/VerseSlideshow';
28+
import DatePicker from '@/components/ui/date-picker';
2529

2630
const formatDate = (date: Date) => {
2731
return new Intl.DateTimeFormat('en-US', {
@@ -36,6 +40,8 @@ const Dashboard: React.FC = () => {
3640
const [streak, setStreak] = useState(0);
3741
const [lastCheckIn, setLastCheckIn] = useState<Date | null>(null);
3842
const [isCheckedInToday, setIsCheckedInToday] = useState(false);
43+
const [isCheckInSide, setIsCheckInSide] = useState(true);
44+
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
3945

4046
const featuredMeditations = [
4147
{
@@ -78,9 +84,10 @@ const Dashboard: React.FC = () => {
7884

7985
const handleCheckIn = async () => {
8086
if (!currentUser) return;
81-
87+
console.log(currentUser.uid)
8288
try {
8389
const result = await updateStreak(currentUser.uid);
90+
console.log(result)
8491

8592
if (result.success) {
8693
const updatedProfile = await getUserProfile(currentUser.uid);
@@ -109,6 +116,44 @@ const Dashboard: React.FC = () => {
109116
}
110117
};
111118

119+
const handleStreakSet = async () => {
120+
if (!currentUser) return;
121+
122+
try {
123+
const result = await updateStreakStart(currentUser.uid, selectedDate);
124+
125+
if (result.success) {
126+
// Refresh user data
127+
const updatedProfile = await getUserProfile(currentUser.uid);
128+
129+
if (updatedProfile) {
130+
setStreak(updatedProfile.streakDays || 0);
131+
}
132+
133+
if (result.message === 'Streak start updated successfully') {
134+
toast.success("Streak start updated!", {
135+
description: `Your streak has been reset to start from ${formatDate(selectedDate)}.`,
136+
});
137+
}
138+
}
139+
140+
if (!result.success && result.message === 'Invalid Date') {
141+
toast.error("Invalid date selected", {
142+
description: "Please select a valid date to start your streak.",
143+
});
144+
}
145+
146+
} catch (error) {
147+
console.error('Error updating streak:', error);
148+
toast.error("Failed to set streak start date", {
149+
description: "Please try again later.",
150+
});
151+
} finally {
152+
setIsCheckInSide(true);
153+
}
154+
};
155+
156+
112157
return (
113158
<motion.div
114159
className="container py-8 pb-16"
@@ -145,40 +190,96 @@ const Dashboard: React.FC = () => {
145190
"border-primary/20 h-full",
146191
streak > 0 && "bg-gradient-to-br from-primary/10 to-transparent"
147192
)}>
148-
<CardHeader className="pb-2">
149-
<CardTitle className="flex items-center">
150-
<Trophy className="h-5 w-5 mr-2 text-primary" />
151-
Your Current Streak
152-
</CardTitle>
153-
<CardDescription>
154-
{isCheckedInToday
155-
? 'You\'ve checked in today. Great job!'
156-
: 'Remember to check in daily to maintain your streak'}
157-
</CardDescription>
158-
</CardHeader>
159-
<CardContent>
160-
<div className="flex items-center justify-center py-6">
161-
<div className="text-center">
162-
<div className="text-5xl font-bold mb-2 text-primary">
163-
{streak}
164-
</div>
165-
<div className="text-muted-foreground">
166-
consecutive {streak === 1 ? 'day' : 'days'}
193+
{/* Current Streak Card - Check In Side */}
194+
{isCheckInSide ? (
195+
<motion.div
196+
key="check-in-side"
197+
initial={{ opacity: 0 }}
198+
animate={{ opacity: 1 }}
199+
transition={{ duration: 0.5 }}
200+
>
201+
<CardHeader className="pb-2">
202+
<CardTitle className="flex items-center">
203+
<Trophy className="h-5 w-5 mr-2 text-primary" />
204+
Your Current Streak
205+
</CardTitle>
206+
<CardDescription>
207+
{isCheckedInToday
208+
? 'You\'ve checked in today. Great job!'
209+
: 'Remember to check in daily to maintain your streak'}
210+
</CardDescription>
211+
</CardHeader>
212+
<CardContent>
213+
<div className="flex items-center justify-center py-6">
214+
<div className="text-center">
215+
<div className="text-5xl font-bold mb-2 text-primary">
216+
{streak}
217+
</div>
218+
<div className="text-muted-foreground">
219+
consecutive {streak === 1 ? 'day' : 'days'}
220+
</div>
221+
</div>
167222
</div>
168-
</div>
169-
</div>
170-
</CardContent>
171-
<CardFooter>
172-
<Button
173-
onClick={handleCheckIn}
174-
disabled={isCheckedInToday}
175-
variant={isCheckedInToday ? "outline" : "default"}
176-
className="w-full"
223+
</CardContent>
224+
<CardFooter className="flex flex-col items-center space-y-2">
225+
<Button
226+
onClick={handleCheckIn}
227+
disabled={isCheckedInToday}
228+
variant={isCheckedInToday ? "outline" : "default"}
229+
className="w-full"
230+
>
231+
{isCheckedInToday ? 'Already Checked In Today' : 'Check In for Today'}
232+
{!isCheckedInToday && <Calendar className="ml-2 h-4 w-4" />}
233+
</Button>
234+
<Button
235+
variant="outline"
236+
className="w-full text-xs"
237+
onClick={() => setIsCheckInSide(false)}
238+
>
239+
Edit Streak Start Date
240+
</Button>
241+
</CardFooter>
242+
</motion.div>
243+
) : (
244+
// Current Streak Card - Set Streak Start Date Side
245+
<motion.div
246+
key="streak-start-side"
247+
initial={{ opacity: 0 }}
248+
animate={{ opacity: 1 }}
249+
transition={{ duration: 0.5 }}
177250
>
178-
{isCheckedInToday ? 'Already Checked In Today' : 'Check In for Today'}
179-
{!isCheckedInToday && <Calendar className="ml-2 h-4 w-4" />}
180-
</Button>
181-
</CardFooter>
251+
<CardHeader className="pb-2">
252+
<CardTitle className="flex items-center">
253+
<Trophy className="h-5 w-5 mr-2 text-primary" />
254+
Set Your Streak
255+
</CardTitle>
256+
<CardDescription>
257+
Change Your Streak Start Date
258+
</CardDescription>
259+
</CardHeader>
260+
<CardContent>
261+
<div className="flex items-center justify-center">
262+
{/* calculate the streak start date to display in DatePicker */}
263+
<DatePicker onDateChange={setSelectedDate} preselectedDate={new Date(Date.now() - streak * 24 * 60 * 60 * 1000)} />
264+
</div>
265+
</CardContent>
266+
<CardFooter className="flex flex-col items-center space-y-2">
267+
<Button
268+
onClick={handleStreakSet}
269+
className="w-full"
270+
>
271+
Set Streak Start Date <CheckIcon />
272+
</Button>
273+
<Button
274+
variant="outline"
275+
className="w-full text-xs"
276+
onClick={() => setIsCheckInSide(true)}
277+
>
278+
Cancel
279+
</Button>
280+
</CardFooter>
281+
</motion.div>
282+
)}
182283
</Card>
183284
</motion.div>
184285

0 commit comments

Comments
 (0)