PrintCalendar.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { useMemo } from 'react';
  2. interface PrintCalendarProps {
  3. printDates: string[]; // Array of ISO date strings
  4. months?: number; // How many months to show (default 3)
  5. }
  6. export function PrintCalendar({ printDates, months = 3 }: PrintCalendarProps) {
  7. const { weeks, monthLabels, printCounts } = useMemo(() => {
  8. // Count prints per day
  9. const counts: Record<string, number> = {};
  10. printDates.forEach((date) => {
  11. const day = date.split('T')[0];
  12. counts[day] = (counts[day] || 0) + 1;
  13. });
  14. // Generate weeks for the last N months
  15. const today = new Date();
  16. const startDate = new Date(today);
  17. startDate.setMonth(startDate.getMonth() - months);
  18. startDate.setDate(startDate.getDate() - startDate.getDay()); // Start from Sunday
  19. const weeks: Date[][] = [];
  20. const monthLabels: { month: string; weekIndex: number }[] = [];
  21. let currentWeek: Date[] = [];
  22. let lastMonth = -1;
  23. const current = new Date(startDate);
  24. let weekIndex = 0;
  25. while (current <= today) {
  26. if (current.getDay() === 0 && currentWeek.length > 0) {
  27. weeks.push(currentWeek);
  28. currentWeek = [];
  29. weekIndex++;
  30. }
  31. // Track month labels
  32. if (current.getMonth() !== lastMonth) {
  33. monthLabels.push({
  34. month: current.toLocaleDateString('en-US', { month: 'short' }),
  35. weekIndex,
  36. });
  37. lastMonth = current.getMonth();
  38. }
  39. currentWeek.push(new Date(current));
  40. current.setDate(current.getDate() + 1);
  41. }
  42. if (currentWeek.length > 0) {
  43. weeks.push(currentWeek);
  44. }
  45. return { weeks, monthLabels, printCounts: counts };
  46. }, [printDates, months]);
  47. const maxCount = Math.max(1, ...Object.values(printCounts));
  48. const getColor = (count: number) => {
  49. if (count === 0) return 'bg-bambu-dark';
  50. const intensity = count / maxCount;
  51. if (intensity <= 0.25) return 'bg-bambu-green/30';
  52. if (intensity <= 0.5) return 'bg-bambu-green/50';
  53. if (intensity <= 0.75) return 'bg-bambu-green/75';
  54. return 'bg-bambu-green';
  55. };
  56. const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  57. return (
  58. <div className="overflow-x-auto">
  59. {/* Month labels */}
  60. <div className="flex mb-1 ml-8">
  61. {monthLabels.map(({ month, weekIndex }, i) => (
  62. <div
  63. key={i}
  64. className="text-xs text-bambu-gray"
  65. style={{ marginLeft: i === 0 ? 0 : `${(weekIndex - (monthLabels[i - 1]?.weekIndex || 0)) * 14 - 24}px` }}
  66. >
  67. {month}
  68. </div>
  69. ))}
  70. </div>
  71. <div className="flex gap-0.5">
  72. {/* Day labels */}
  73. <div className="flex flex-col gap-0.5 mr-1">
  74. {dayLabels.map((day, i) => (
  75. <div
  76. key={day}
  77. className="h-3 text-xs text-bambu-gray flex items-center"
  78. style={{ visibility: i % 2 === 1 ? 'visible' : 'hidden' }}
  79. >
  80. {day}
  81. </div>
  82. ))}
  83. </div>
  84. {/* Calendar grid */}
  85. {weeks.map((week, weekIndex) => (
  86. <div key={weekIndex} className="flex flex-col gap-0.5">
  87. {[0, 1, 2, 3, 4, 5, 6].map((dayOfWeek) => {
  88. const day = week.find((d) => d.getDay() === dayOfWeek);
  89. if (!day) {
  90. return <div key={dayOfWeek} className="w-3 h-3" />;
  91. }
  92. const dateStr = day.toISOString().split('T')[0];
  93. const count = printCounts[dateStr] || 0;
  94. const isToday = dateStr === new Date().toISOString().split('T')[0];
  95. return (
  96. <div
  97. key={dayOfWeek}
  98. className={`w-3 h-3 rounded-sm ${getColor(count)} ${isToday ? 'ring-1 ring-white' : ''}`}
  99. title={`${day.toLocaleDateString()}: ${count} print${count !== 1 ? 's' : ''}`}
  100. />
  101. );
  102. })}
  103. </div>
  104. ))}
  105. </div>
  106. {/* Legend */}
  107. <div className="flex items-center gap-2 mt-3 text-xs text-bambu-gray">
  108. <span>Less</span>
  109. <div className="flex gap-0.5">
  110. <div className="w-3 h-3 rounded-sm bg-bambu-dark" />
  111. <div className="w-3 h-3 rounded-sm bg-bambu-green/30" />
  112. <div className="w-3 h-3 rounded-sm bg-bambu-green/50" />
  113. <div className="w-3 h-3 rounded-sm bg-bambu-green/75" />
  114. <div className="w-3 h-3 rounded-sm bg-bambu-green" />
  115. </div>
  116. <span>More</span>
  117. </div>
  118. </div>
  119. );
  120. }