This repository has no description
0

Configure Feed

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

chart improvements

+173 -51
+9 -3
src/components/CollectionsFeed/ActivityChart.css
··· 13 13 justify-content: center; 14 14 margin-bottom: 15px; 15 15 flex-wrap: wrap; 16 - gap: 10px; 16 + gap: 8px; 17 + max-width: 90%; 18 + margin-left: auto; 19 + margin-right: auto; 17 20 } 18 21 19 22 .time-period-button { ··· 21 24 color: var(--text); 22 25 border: 1px solid var(--card-border); 23 26 border-radius: 4px; 24 - padding: 8px 12px; 25 - font-size: 0.9rem; 27 + padding: 8px 10px; 28 + font-size: 0.85rem; 26 29 cursor: pointer; 27 30 transition: all 0.2s ease; 31 + flex: 1; 32 + min-width: 90px; 33 + white-space: nowrap; 28 34 } 29 35 30 36 .time-period-button:hover {
+164 -48
src/components/CollectionsFeed/ActivityChart.js
··· 28 28 datasets: [] 29 29 }); 30 30 31 + // App color scheme 32 + const bskyColor = 'rgba(0, 133, 255, 0.7)'; // Lighter blue for Bluesky 33 + const bskyBorderColor = 'rgba(0, 133, 255, 1)'; 34 + const atprotoColor = 'rgba(0, 51, 102, 0.8)'; // Darker blue for ATProto 35 + const atprotoBorderColor = 'rgba(0, 51, 102, 1)'; 36 + 31 37 useEffect(() => { 32 38 // Only generate chart data if we have records 33 39 if (records && records.length > 0) { ··· 41 47 const currentDate = new Date(); 42 48 let startDate; 43 49 let dateFormat; 50 + let bucketSize; 51 + let timeFormat; 44 52 45 53 switch (period) { 54 + case '24hours': 55 + startDate = new Date(currentDate); 56 + startDate.setHours(currentDate.getHours() - 24); 57 + dateFormat = { hour: '2-digit' }; // "05 PM" 58 + bucketSize = 'hour'; 59 + timeFormat = true; 60 + break; 46 61 case '7days': 47 62 startDate = new Date(currentDate); 48 63 startDate.setDate(currentDate.getDate() - 7); 49 64 dateFormat = { month: 'short', day: 'numeric' }; // "Jan 1" 65 + bucketSize = 'day'; 66 + timeFormat = false; 50 67 break; 51 68 case '30days': 52 69 startDate = new Date(currentDate); 53 70 startDate.setDate(currentDate.getDate() - 30); 54 71 dateFormat = { month: 'short', day: 'numeric' }; // "Jan 1" 72 + bucketSize = 'day'; 73 + timeFormat = false; 55 74 break; 56 75 case '90days': 57 76 startDate = new Date(currentDate); 58 77 startDate.setDate(currentDate.getDate() - 90); 59 - dateFormat = { month: 'short' }; // "January" 78 + // For 90 days, group by week instead of day to make it more readable 79 + dateFormat = { month: 'short', day: 'numeric' }; 80 + bucketSize = 'week'; 81 + timeFormat = false; 60 82 break; 61 83 default: 62 84 startDate = new Date(currentDate); 63 85 startDate.setDate(currentDate.getDate() - 7); 64 86 dateFormat = { month: 'short', day: 'numeric' }; // "Jan 1" 87 + bucketSize = 'day'; 88 + timeFormat = false; 65 89 } 66 90 67 91 // Create date buckets 68 92 const dateBuckets = {}; 69 93 const labels = []; 70 94 71 - // Initialize date buckets based on the selected time period 72 - let currentBucket = new Date(startDate); 73 - 74 - while (currentBucket <= currentDate) { 75 - const dateKey = currentBucket.toISOString().split('T')[0]; // YYYY-MM-DD 76 - const formattedDate = currentBucket.toLocaleDateString('en-US', dateFormat); 95 + // For 90 days with weekly buckets, calculate week numbers 96 + if (bucketSize === 'week') { 97 + // Create weekly buckets 98 + let currentWeekStart = new Date(startDate); 99 + // Adjust to start on Sunday or Monday (Sunday = 0, Monday = 1) 100 + const dayOfWeek = currentWeekStart.getDay(); 101 + if (dayOfWeek !== 0) { // If not Sunday 102 + // Adjust date to previous Sunday 103 + currentWeekStart.setDate(currentWeekStart.getDate() - dayOfWeek); 104 + } 77 105 78 - dateBuckets[dateKey] = { 79 - date: formattedDate, 80 - total: 0, 81 - bskyRecords: 0, 82 - nonBskyRecords: 0 83 - }; 106 + while (currentWeekStart <= currentDate) { 107 + const weekEndDate = new Date(currentWeekStart); 108 + weekEndDate.setDate(weekEndDate.getDate() + 6); // End date is 6 days after start (for a full week) 109 + 110 + const bucketKey = currentWeekStart.toISOString().split('T')[0]; 111 + const startLabel = currentWeekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 112 + const endLabel = weekEndDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 113 + const weekLabel = `${startLabel} - ${endLabel}`; 114 + 115 + dateBuckets[bucketKey] = { 116 + label: weekLabel, 117 + start: new Date(currentWeekStart), // Store start and end dates for filtering 118 + end: new Date(weekEndDate), 119 + bskyRecords: 0, 120 + atprotoRecords: 0 121 + }; 122 + 123 + labels.push(weekLabel); 124 + 125 + // Move to next week 126 + currentWeekStart.setDate(currentWeekStart.getDate() + 7); 127 + } 128 + } 129 + else if (bucketSize === 'hour') { 130 + // Create hourly buckets for 24-hour view 131 + let currentHour = new Date(startDate); 132 + currentHour.setMinutes(0, 0, 0); // Start at the beginning of the hour 84 133 85 - labels.push(formattedDate); 134 + while (currentHour <= currentDate) { 135 + const hourKey = currentHour.toISOString(); 136 + let hourLabel; 137 + 138 + if (timeFormat) { 139 + hourLabel = currentHour.toLocaleTimeString('en-US', { hour: '2-digit' }); 140 + } else { 141 + hourLabel = currentHour.toLocaleDateString('en-US', dateFormat); 142 + } 143 + 144 + dateBuckets[hourKey] = { 145 + label: hourLabel, 146 + timestamp: new Date(currentHour), 147 + bskyRecords: 0, 148 + atprotoRecords: 0 149 + }; 150 + 151 + labels.push(hourLabel); 152 + 153 + // Move to next hour 154 + currentHour.setHours(currentHour.getHours() + 1); 155 + } 156 + } 157 + else { 158 + // Create daily buckets for 7-day and 30-day views 159 + let currentDay = new Date(startDate); 160 + currentDay.setHours(0, 0, 0, 0); // Start at the beginning of the day 86 161 87 - // Move to next day 88 - currentBucket.setDate(currentBucket.getDate() + 1); 162 + while (currentDay <= currentDate) { 163 + const dayKey = currentDay.toISOString().split('T')[0]; // YYYY-MM-DD 164 + const dayLabel = currentDay.toLocaleDateString('en-US', dateFormat); 165 + 166 + dateBuckets[dayKey] = { 167 + label: dayLabel, 168 + date: new Date(currentDay), 169 + bskyRecords: 0, 170 + atprotoRecords: 0 171 + }; 172 + 173 + labels.push(dayLabel); 174 + 175 + // Move to next day 176 + currentDay.setDate(currentDay.getDate() + 1); 177 + } 89 178 } 90 179 91 - // Count records for each date 180 + // Count records for each bucket 92 181 allRecords.forEach(record => { 93 182 // Use either content timestamp or rkey timestamp, prioritizing content 94 183 const timestamp = record.contentTimestamp || record.rkeyTimestamp; 95 184 if (!timestamp) return; 96 185 97 186 const recordDate = new Date(timestamp); 98 - const dateKey = recordDate.toISOString().split('T')[0]; // YYYY-MM-DD 187 + 188 + // Find the matching bucket based on the time period type 189 + let matchingBucketKey = null; 190 + 191 + if (bucketSize === 'week') { 192 + // For weekly buckets, find the week that contains this record 193 + for (const bucketKey in dateBuckets) { 194 + const bucket = dateBuckets[bucketKey]; 195 + if (recordDate >= bucket.start && recordDate <= bucket.end) { 196 + matchingBucketKey = bucketKey; 197 + break; 198 + } 199 + } 200 + } 201 + else if (bucketSize === 'hour') { 202 + // For hourly buckets, find the hour 203 + const hourStart = new Date(recordDate); 204 + hourStart.setMinutes(0, 0, 0); 205 + matchingBucketKey = hourStart.toISOString(); 206 + } 207 + else { 208 + // For daily buckets, use the date key 209 + matchingBucketKey = recordDate.toISOString().split('T')[0]; // YYYY-MM-DD 210 + } 99 211 100 212 // Only count if within our date range 101 - if (dateBuckets[dateKey]) { 102 - dateBuckets[dateKey].total += 1; 103 - 104 - // Also track Bluesky vs non-Bluesky records 213 + if (matchingBucketKey && dateBuckets[matchingBucketKey]) { 214 + // Track Bluesky vs non-Bluesky records 105 215 if (record.collection.startsWith('app.bsky.')) { 106 - dateBuckets[dateKey].bskyRecords += 1; 216 + dateBuckets[matchingBucketKey].bskyRecords += 1; 107 217 } else { 108 - dateBuckets[dateKey].nonBskyRecords += 1; 218 + dateBuckets[matchingBucketKey].atprotoRecords += 1; 109 219 } 110 220 } 111 221 }); 112 222 113 223 // Format data for Chart.js 114 - const totalData = []; 115 224 const bskyData = []; 116 - const nonBskyData = []; 225 + const atprotoData = []; 117 226 118 227 // Extract data in the same order as labels 119 228 labels.forEach(label => { 120 - // Find the matching bucket by formatted date 121 - const bucket = Object.values(dateBuckets).find(b => b.date === label); 229 + // Find the matching bucket by label 230 + const bucket = Object.values(dateBuckets).find(b => b.label === label); 122 231 123 232 if (bucket) { 124 - totalData.push(bucket.total); 125 233 bskyData.push(bucket.bskyRecords); 126 - nonBskyData.push(bucket.nonBskyRecords); 234 + atprotoData.push(bucket.atprotoRecords); 127 235 } else { 128 236 // Fallback (shouldn't happen) 129 - totalData.push(0); 130 237 bskyData.push(0); 131 - nonBskyData.push(0); 238 + atprotoData.push(0); 132 239 } 133 240 }); 134 241 135 - // Set the chart data 242 + // Set the chart data for a stacked chart 136 243 setChartData({ 137 244 labels, 138 245 datasets: [ 139 246 { 140 - label: 'All Records', 141 - data: totalData, 142 - backgroundColor: 'rgba(0, 133, 255, 0.6)', 143 - borderColor: 'rgba(0, 133, 255, 1)', 144 - borderWidth: 1 145 - }, 146 - { 147 247 label: 'Bluesky Records', 148 248 data: bskyData, 149 - backgroundColor: 'rgba(75, 192, 192, 0.6)', 150 - borderColor: 'rgba(75, 192, 192, 1)', 249 + backgroundColor: bskyColor, 250 + borderColor: bskyBorderColor, 151 251 borderWidth: 1 152 252 }, 153 253 { 154 254 label: 'Other ATProto Records', 155 - data: nonBskyData, 156 - backgroundColor: 'rgba(153, 102, 255, 0.6)', 157 - borderColor: 'rgba(153, 102, 255, 1)', 255 + data: atprotoData, 256 + backgroundColor: atprotoColor, 257 + borderColor: atprotoBorderColor, 158 258 borderWidth: 1 159 259 } 160 260 ] 161 261 }); 162 262 }; 163 263 164 - // Chart options 264 + // Chart options for stacked bar 165 265 const options = { 166 266 responsive: true, 167 267 maintainAspectRatio: false, ··· 171 271 }, 172 272 title: { 173 273 display: true, 174 - text: 'ATProto Activity by Date' 274 + text: 'ATProto Activity' 175 275 }, 176 276 tooltip: { 177 277 callbacks: { ··· 182 282 const label = context.dataset.label || ''; 183 283 const value = context.raw || 0; 184 284 return `${label}: ${value} record${value !== 1 ? 's' : ''}`; 285 + }, 286 + // Add footer for total 287 + footer: (tooltipItems) => { 288 + let sum = 0; 289 + tooltipItems.forEach(tooltipItem => { 290 + sum += tooltipItem.parsed.y; 291 + }); 292 + return `Total: ${sum} record${sum !== 1 ? 's' : ''}`; 185 293 } 186 294 } 187 295 } 188 296 }, 189 297 scales: { 190 298 x: { 299 + stacked: true, 191 300 title: { 192 301 display: true, 193 - text: 'Date' 302 + text: timePeriod === '24hours' ? 'Hour' : 'Date' 194 303 } 195 304 }, 196 305 y: { 306 + stacked: true, 197 307 beginAtZero: true, 198 308 title: { 199 309 display: true, ··· 217 327 return ( 218 328 <div className="activity-chart-container"> 219 329 <div className="time-period-selector"> 330 + <button 331 + className={`time-period-button ${timePeriod === '24hours' ? 'active' : ''}`} 332 + onClick={() => setTimePeriod('24hours')} 333 + > 334 + Last 24 Hours 335 + </button> 220 336 <button 221 337 className={`time-period-button ${timePeriod === '7days' ? 'active' : ''}`} 222 338 onClick={() => setTimePeriod('7days')}