|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import React, { useState } from "react"; |
4 | | -import Image from "next/image"; |
5 | 4 | import Link from "next/link"; |
6 | 5 | import { useRouter } from "next/navigation"; |
7 | 6 | import ShareOptionsSheet from "../../components/ShareOptionsSheet"; |
8 | 7 | 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"; |
207 | 9 |
|
208 | 10 | interface StreakSummaryCardProps { |
209 | 11 | streakCount: number; |
@@ -386,6 +188,7 @@ export default function StreakPage() { |
386 | 188 | Streak Calendar |
387 | 189 | </h2> |
388 | 190 | <StreakCalendar |
| 191 | + variant="panel" |
389 | 192 | currentMonth={new Date()} |
390 | 193 | streakData={DEMO_STREAK_DATA} |
391 | 194 | /> |
|
0 commit comments