this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add month and week grouping to days list

- Group days by month with headers
- Group within months by week with divider labels
- Show relative dates (Today, Yesterday, This Week, Last Week)
- Highlight today's entry with blue icon

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice d8784f4f 29be652b

+162 -30
+162 -30
src/web/app/components/DayList.tsx
··· 2 2 import { Link } from 'react-router-dom'; 3 3 import { Calendar, ChevronRight, Layers, Clock } from 'lucide-react'; 4 4 import { useDays } from '../hooks/useWorklog'; 5 + import type { DayListItem } from '../../../../types'; 6 + 7 + // Get ISO week number 8 + function getWeekNumber(date: Date): number { 9 + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); 10 + const dayNum = d.getUTCDay() || 7; 11 + d.setUTCDate(d.getUTCDate() + 4 - dayNum); 12 + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); 13 + return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); 14 + } 15 + 16 + // Get week start date for display 17 + function getWeekStart(date: Date): Date { 18 + const d = new Date(date); 19 + const day = d.getDay(); 20 + const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday start 21 + return new Date(d.setDate(diff)); 22 + } 23 + 24 + interface GroupedDays { 25 + month: string; 26 + monthKey: string; 27 + weeks: { 28 + weekKey: string; 29 + weekLabel: string; 30 + days: DayListItem[]; 31 + }[]; 32 + } 33 + 34 + function groupDaysByMonthAndWeek(days: DayListItem[]): GroupedDays[] { 35 + const today = new Date(); 36 + const thisWeek = getWeekNumber(today); 37 + const thisYear = today.getFullYear(); 38 + const thisMonth = today.getMonth(); 39 + 40 + const grouped = new Map<string, Map<string, DayListItem[]>>(); 41 + 42 + for (const day of days) { 43 + const date = new Date(day.date + 'T12:00:00'); // Noon to avoid timezone issues 44 + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; 45 + const weekNum = getWeekNumber(date); 46 + const weekKey = `${date.getFullYear()}-W${String(weekNum).padStart(2, '0')}`; 47 + 48 + if (!grouped.has(monthKey)) { 49 + grouped.set(monthKey, new Map()); 50 + } 51 + const monthMap = grouped.get(monthKey)!; 52 + if (!monthMap.has(weekKey)) { 53 + monthMap.set(weekKey, []); 54 + } 55 + monthMap.get(weekKey)!.push(day); 56 + } 57 + 58 + const result: GroupedDays[] = []; 59 + 60 + for (const [monthKey, weekMap] of grouped) { 61 + const [year, month] = monthKey.split('-').map(Number); 62 + const monthDate = new Date(year, month - 1, 1); 63 + const isCurrentMonth = year === thisYear && month - 1 === thisMonth; 64 + const monthLabel = isCurrentMonth 65 + ? 'This Month' 66 + : monthDate.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); 67 + 68 + const weeks: GroupedDays['weeks'] = []; 69 + 70 + for (const [weekKey, weekDays] of weekMap) { 71 + const [weekYear, weekStr] = weekKey.split('-W'); 72 + const weekNum = parseInt(weekStr); 73 + const isCurrentWeek = parseInt(weekYear) === thisYear && weekNum === thisWeek; 74 + const isLastWeek = parseInt(weekYear) === thisYear && weekNum === thisWeek - 1; 75 + 76 + let weekLabel: string; 77 + if (isCurrentWeek) { 78 + weekLabel = 'This Week'; 79 + } else if (isLastWeek) { 80 + weekLabel = 'Last Week'; 81 + } else { 82 + const firstDay = weekDays[weekDays.length - 1]; // Last in sorted order = earliest 83 + const weekStart = getWeekStart(new Date(firstDay.date + 'T12:00:00')); 84 + weekLabel = `Week of ${weekStart.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`; 85 + } 86 + 87 + weeks.push({ weekKey, weekLabel, days: weekDays }); 88 + } 89 + 90 + result.push({ month: monthLabel, monthKey, weeks }); 91 + } 92 + 93 + return result; 94 + } 95 + 96 + function DayCard({ day }: { day: DayListItem }) { 97 + const date = new Date(day.date + 'T12:00:00'); 98 + const today = new Date(); 99 + const yesterday = new Date(today); 100 + yesterday.setDate(yesterday.getDate() - 1); 101 + 102 + const isToday = day.date === today.toISOString().split('T')[0]; 103 + const isYesterday = day.date === yesterday.toISOString().split('T')[0]; 104 + 105 + let dateLabel: string; 106 + if (isToday) { 107 + dateLabel = 'Today'; 108 + } else if (isYesterday) { 109 + dateLabel = 'Yesterday'; 110 + } else { 111 + dateLabel = date.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' }); 112 + } 113 + 114 + return ( 115 + <Link to={`/day/${day.date}`} className="block group"> 116 + <div className="bg-white rounded-lg p-3 border border-gray-200 shadow-sm hover:shadow-md hover:border-blue-200 transition-all flex items-center justify-between"> 117 + <div className="flex items-center gap-3"> 118 + <div className={`p-2 rounded-md transition-colors ${ 119 + isToday 120 + ? 'bg-blue-600 text-white' 121 + : 'bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white' 122 + }`}> 123 + <Calendar size={18} /> 124 + </div> 125 + <div> 126 + <h3 className="text-base font-semibold text-slate-800">{dateLabel}</h3> 127 + <div className="flex items-center gap-3 text-xs text-slate-500"> 128 + <div className="flex items-center gap-1.5"> 129 + <Layers size={14} /> 130 + <span>{day.projectCount} Projects</span> 131 + </div> 132 + <div className="flex items-center gap-1.5"> 133 + <Clock size={14} /> 134 + <span>{day.sessionCount} Sessions</span> 135 + </div> 136 + </div> 137 + </div> 138 + </div> 139 + <ChevronRight className="text-gray-300 group-hover:text-blue-500 transition-colors" /> 140 + </div> 141 + </Link> 142 + ); 143 + } 5 144 6 145 export default function DayList() { 7 146 const { days, loading, error } = useDays(); ··· 19 158 ); 20 159 } 21 160 161 + const grouped = groupDaysByMonthAndWeek(days); 162 + 22 163 return ( 23 164 <div className="max-w-3xl mx-auto"> 24 - <h2 className="text-2xl font-bold text-slate-800 mb-6">Session History</h2> 25 - <div className="grid gap-4"> 26 - {days.map((day) => ( 27 - <Link 28 - key={day.date} 29 - to={`/day/${day.date}`} 30 - className="block group" 31 - > 32 - <div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm hover:shadow-md hover:border-blue-200 transition-all flex items-center justify-between"> 33 - <div className="flex items-center gap-4"> 34 - <div className="bg-blue-50 text-blue-600 p-3 rounded-lg group-hover:bg-blue-600 group-hover:text-white transition-colors"> 35 - <Calendar size={24} /> 36 - </div> 37 - <div> 38 - <h3 className="text-lg font-semibold text-slate-800"> 39 - {new Date(day.date).toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} 40 - </h3> 41 - <div className="flex items-center gap-4 mt-1 text-sm text-slate-500"> 42 - <div className="flex items-center gap-1.5"> 43 - <Layers size={14} /> 44 - <span>{day.projectCount} Projects</span> 45 - </div> 46 - <div className="flex items-center gap-1.5"> 47 - <Clock size={14} /> 48 - <span>{day.sessionCount} Sessions</span> 49 - </div> 165 + <h2 className="text-xl font-bold text-slate-800 mb-4">Session History</h2> 166 + <div className="space-y-6"> 167 + {grouped.map((monthGroup) => ( 168 + <div key={monthGroup.monthKey}> 169 + <h3 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-3 px-1"> 170 + {monthGroup.month} 171 + </h3> 172 + <div className="space-y-4"> 173 + {monthGroup.weeks.map((weekGroup) => ( 174 + <div key={weekGroup.weekKey}> 175 + <div className="flex items-center gap-2 mb-2 px-1"> 176 + <div className="h-px bg-slate-200 flex-1" /> 177 + <span className="text-xs text-slate-400 font-medium">{weekGroup.weekLabel}</span> 178 + <div className="h-px bg-slate-200 flex-1" /> 179 + </div> 180 + <div className="grid gap-2"> 181 + {weekGroup.days.map((day) => ( 182 + <DayCard key={day.date} day={day} /> 183 + ))} 50 184 </div> 51 185 </div> 52 - </div> 53 - 54 - <ChevronRight className="text-gray-300 group-hover:text-blue-500 transition-colors" /> 186 + ))} 55 187 </div> 56 - </Link> 188 + </div> 57 189 ))} 58 190 </div> 59 191 </div>