Skip to content

Commit 6f3058d

Browse files
authored
Merge pull request #474 from anumukul/feat/streak-calendar-month-navigation
feat(streak): memoized month navigation for streak calendar
2 parents 8c078f6 + 908a535 commit 6f3058d

File tree

2 files changed

+155
-298
lines changed

2 files changed

+155
-298
lines changed

frontend/app/streak/page.tsx

Lines changed: 2 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -1,209 +1,11 @@
11
"use client";
22

33
import React, { useState } from "react";
4-
import Image from "next/image";
54
import Link from "next/link";
65
import { useRouter } from "next/navigation";
76
import ShareOptionsSheet from "../../components/ShareOptionsSheet";
87
import ShareStreakCard from "@/components/ShareStreakCard";
9-
10-
export interface StreakData {
11-
[date: string]: {
12-
completed: boolean;
13-
inStreak?: boolean;
14-
missed?: boolean;
15-
};
16-
}
17-
18-
export interface DayData {
19-
day: string;
20-
completed: boolean;
21-
}
22-
23-
interface StreakDayIndicatorProps {
24-
status: "empty" | "completed" | "streak" | "missed";
25-
isToday?: boolean;
26-
inStreakRun?: boolean;
27-
}
28-
29-
const StreakDayIndicator: React.FC<StreakDayIndicatorProps> = ({
30-
status,
31-
isToday = false,
32-
inStreakRun = false,
33-
}) => {
34-
const baseClasses =
35-
"flex items-center justify-center w-[24px] h-[24px] md:w-[28px] md:h-[28px] rounded-full z-10 shrink-0";
36-
37-
let statusClasses = "";
38-
if (status === "empty") statusClasses = "bg-[#E6E6E6]/20";
39-
else if (status === "completed") statusClasses = "bg-[#FACC15]";
40-
else if (status === "streak")
41-
statusClasses = "bg-[#FACC15] shadow-lg shadow-[#FACC15]/50";
42-
else if (status === "missed") statusClasses = "bg-white/30";
43-
44-
const todayClasses = isToday
45-
? "ring-2 ring-white ring-offset-2 ring-offset-[#050C16]"
46-
: "";
47-
48-
return (
49-
<div className="relative flex items-center justify-center w-[24px] h-[24px] md:w-[28px] md:h-[28px]">
50-
{inStreakRun && status === "streak" && (
51-
<div className="absolute w-[300%] h-[8px] bg-[#FACC15]/20 rounded-full z-0" />
52-
)}
53-
<div className={`${baseClasses} ${statusClasses} ${todayClasses}`}>
54-
{status === "streak" && (
55-
<span className="text-[10px]">🔥</span>
56-
)}
57-
</div>
58-
</div>
59-
);
60-
};
61-
62-
interface StreakCalendarProps {
63-
currentMonth: Date;
64-
streakData: StreakData;
65-
onMonthChange?: (date: Date) => void;
66-
}
67-
68-
const StreakCalendar: React.FC<StreakCalendarProps> = ({
69-
currentMonth,
70-
streakData,
71-
onMonthChange,
72-
}) => {
73-
const [selectedMonth, setSelectedMonth] = useState(currentMonth);
74-
75-
const weekDays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
76-
const monthNames = [
77-
"January", "February", "March", "April", "May", "June",
78-
"July", "August", "September", "October", "November", "December",
79-
];
80-
81-
const getDaysInMonth = (date: Date) =>
82-
new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
83-
84-
const getFirstDayOfMonth = (date: Date) => {
85-
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1).getDay();
86-
return firstDay === 0 ? 6 : firstDay - 1;
87-
};
88-
89-
const formatDateKey = (year: number, month: number, day: number) =>
90-
`${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
91-
92-
const isToday = (year: number, month: number, day: number) => {
93-
const today = new Date();
94-
return (
95-
year === today.getFullYear() &&
96-
month === today.getMonth() &&
97-
day === today.getDate()
98-
);
99-
};
100-
101-
const handlePreviousMonth = () => {
102-
const newMonth = new Date(selectedMonth.getFullYear(), selectedMonth.getMonth() - 1);
103-
setSelectedMonth(newMonth);
104-
onMonthChange?.(newMonth);
105-
};
106-
107-
const handleNextMonth = () => {
108-
const newMonth = new Date(selectedMonth.getFullYear(), selectedMonth.getMonth() + 1);
109-
setSelectedMonth(newMonth);
110-
onMonthChange?.(newMonth);
111-
};
112-
113-
const renderCalendarDays = () => {
114-
const daysInMonth = getDaysInMonth(selectedMonth);
115-
const firstDayOfMonth = getFirstDayOfMonth(selectedMonth);
116-
const year = selectedMonth.getFullYear();
117-
const month = selectedMonth.getMonth();
118-
const days = [];
119-
120-
for (let i = 0; i < firstDayOfMonth; i++) {
121-
days.push(<div key={`empty-${i}`} className="aspect-square" />);
122-
}
123-
124-
for (let day = 1; day <= daysInMonth; day++) {
125-
const dateKey = formatDateKey(year, month, day);
126-
const dayData = streakData[dateKey];
127-
const today = isToday(year, month, day);
128-
129-
let status: "empty" | "completed" | "streak" | "missed" = "empty";
130-
if (dayData?.missed) status = "missed";
131-
else if (dayData?.completed)
132-
status = dayData?.inStreak ? "streak" : "completed";
133-
134-
days.push(
135-
<div
136-
key={day}
137-
className="aspect-square flex flex-col items-center justify-center gap-1"
138-
>
139-
<span
140-
className={`text-xs font-nunito ${
141-
today ? "text-white font-bold" : "text-[#E6E6E6]/60"
142-
}`}
143-
>
144-
{day}
145-
</span>
146-
<StreakDayIndicator
147-
status={status}
148-
isToday={today}
149-
inStreakRun={dayData?.inStreak}
150-
/>
151-
</div>
152-
);
153-
}
154-
155-
return days;
156-
};
157-
158-
return (
159-
<div className="w-full flex flex-col items-center">
160-
<div className="bg-[#050C16] border border-[#FACC15]/20 w-full max-w-[566px] rounded-[16px] p-[16px] md:p-[24px]">
161-
{/* Month Header */}
162-
<div className="flex items-center justify-between mb-[20px]">
163-
<button
164-
onClick={handlePreviousMonth}
165-
className="p-2 rounded-lg hover:bg-[#FACC15]/10 transition-colors cursor-pointer"
166-
aria-label="Previous month"
167-
>
168-
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" className="text-[#FACC15]">
169-
<path d="M12.5 5L7.5 10L12.5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
170-
</svg>
171-
</button>
172-
<h2 className="text-base font-nunito font-bold text-white text-center uppercase tracking-widest">
173-
{monthNames[selectedMonth.getMonth()].slice(0, 3).toUpperCase()}{" "}
174-
{selectedMonth.getFullYear()}
175-
</h2>
176-
<button
177-
onClick={handleNextMonth}
178-
className="p-2 rounded-lg hover:bg-[#FACC15]/10 transition-colors cursor-pointer"
179-
aria-label="Next month"
180-
>
181-
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" className="text-[#FACC15]">
182-
<path d="M7.5 5L12.5 10L7.5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
183-
</svg>
184-
</button>
185-
</div>
186-
187-
{/* Divider */}
188-
<div className="w-full h-px bg-[#FFFFFF]/10 mb-[16px]" />
189-
190-
{/* Weekday Labels */}
191-
<div className="grid grid-cols-7 gap-1 mb-[12px]">
192-
{weekDays.map((day) => (
193-
<div key={day} className="aspect-square flex items-center justify-center">
194-
<span className="text-xs font-nunito font-semibold text-[#E6E6E6]/50 uppercase">
195-
{day}
196-
</span>
197-
</div>
198-
))}
199-
</div>
200-
201-
{/* Calendar Grid */}
202-
<div className="grid grid-cols-7 gap-1">{renderCalendarDays()}</div>
203-
</div>
204-
</div>
205-
);
206-
};
8+
import { StreakCalendar, StreakData } from "@/components/StreakCalendar";
2079

20810
interface StreakSummaryCardProps {
20911
streakCount: number;
@@ -386,6 +188,7 @@ export default function StreakPage() {
386188
Streak Calendar
387189
</h2>
388190
<StreakCalendar
191+
variant="panel"
389192
currentMonth={new Date()}
390193
streakData={DEMO_STREAK_DATA}
391194
/>

0 commit comments

Comments
 (0)