Skip to content

Commit 415d730

Browse files
committed
feat: redesign overview dashboard with streak tracking and interactive stats
- Add new Overview tab with statistics dashboard - Implement streak counter to track consecutive days of usage - Create interactive stats cards showing transcriptions, words, time saved, and productivity - Reorganize sidebar with Account in main section, Advanced/Help at bottom - Fix license status display in sidebar footer - Add clean, modern UI with hover effects and visual feedback - Remove redundant UI elements for cleaner interface
1 parent 8b5d100 commit 415d730

File tree

7 files changed

+920
-62
lines changed

7 files changed

+920
-62
lines changed

src-tauri/tauri.conf.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"center": true,
2121
"visible": true,
2222
"skipTaskbar": true,
23-
"alwaysOnTop": false
23+
"alwaysOnTop": true
2424
}
2525
],
2626
"security": {
@@ -38,11 +38,7 @@
3838
"bundle": {
3939
"publisher": "Ideaplexa LLC",
4040
"active": true,
41-
"targets": [
42-
"nsis",
43-
"app",
44-
"dmg"
45-
],
41+
"targets": ["nsis", "app", "dmg"],
4642
"icon": [
4743
"icons/32x32.png",
4844
"icons/128x128.png",

src/components/ActivityGraph.tsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { useMemo } from "react";
2+
import { cn } from "@/lib/utils";
3+
import { TranscriptionHistory } from "@/types";
4+
5+
interface ActivityGraphProps {
6+
history: TranscriptionHistory[];
7+
weeks?: number;
8+
}
9+
10+
export function ActivityGraph({ history, weeks = 12 }: ActivityGraphProps) {
11+
const activityData = useMemo(() => {
12+
const today = new Date();
13+
today.setHours(23, 59, 59, 999);
14+
15+
// Calculate the grid: always show 'weeks' weeks x 7 days
16+
const weeksData: number[][] = [];
17+
18+
// Start from 'weeks' weeks ago, on a Sunday
19+
const startDate = new Date(today);
20+
startDate.setDate(startDate.getDate() - (weeks * 7) + 1);
21+
// Adjust to Sunday
22+
const dayOfWeek = startDate.getDay();
23+
if (dayOfWeek !== 0) {
24+
startDate.setDate(startDate.getDate() - dayOfWeek);
25+
}
26+
startDate.setHours(0, 0, 0, 0);
27+
28+
// Create a map for quick lookup of history data
29+
const historyMap = new Map<string, number>();
30+
history.forEach(item => {
31+
const date = new Date(item.timestamp);
32+
const dateKey = date.toISOString().split('T')[0];
33+
historyMap.set(dateKey, (historyMap.get(dateKey) || 0) + 1);
34+
});
35+
36+
// Build the grid week by week
37+
for (let w = 0; w < weeks; w++) {
38+
const week: number[] = [];
39+
for (let d = 0; d < 7; d++) {
40+
const currentDate = new Date(startDate);
41+
currentDate.setDate(startDate.getDate() + (w * 7) + d);
42+
43+
// Check if this date is in the future
44+
if (currentDate > today) {
45+
week.push(0); // Future dates shown as empty
46+
} else {
47+
const dateKey = currentDate.toISOString().split('T')[0];
48+
week.push(historyMap.get(dateKey) || 0);
49+
}
50+
}
51+
weeksData.push(week);
52+
}
53+
54+
// Calculate max count for intensity levels
55+
const maxCount = Math.max(1, ...Array.from(historyMap.values()));
56+
57+
return { weeksData, maxCount };
58+
}, [history, weeks]);
59+
60+
const getIntensityClass = (count: number, maxCount: number) => {
61+
if (count === 0) return "bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700"; // Very visible gray!
62+
63+
const intensity = maxCount > 0 ? count / maxCount : 0;
64+
if (intensity > 0.75) return "bg-primary hover:bg-primary/90";
65+
if (intensity > 0.5) return "bg-primary/75 hover:bg-primary/65";
66+
if (intensity > 0.25) return "bg-primary/50 hover:bg-primary/40";
67+
return "bg-primary/25 hover:bg-primary/20";
68+
};
69+
70+
const monthLabels = useMemo(() => {
71+
const labels: { month: string; position: number }[] = [];
72+
const today = new Date();
73+
const startDate = new Date(today);
74+
startDate.setDate(startDate.getDate() - (weeks * 7) + 1);
75+
76+
let currentMonth = -1;
77+
for (let week = 0; week < weeks; week++) {
78+
const weekDate = new Date(startDate);
79+
weekDate.setDate(weekDate.getDate() + (week * 7));
80+
const month = weekDate.getMonth();
81+
82+
if (month !== currentMonth) {
83+
currentMonth = month;
84+
labels.push({
85+
month: weekDate.toLocaleDateString('en-US', { month: 'short' }),
86+
position: week
87+
});
88+
}
89+
}
90+
91+
return labels;
92+
}, [weeks]);
93+
94+
const dayLabels = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
95+
96+
return (
97+
<div className="space-y-2">
98+
<div className="flex items-center justify-between mb-2">
99+
<h3 className="text-sm font-medium">Activity</h3>
100+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
101+
<span>Less</span>
102+
<div className="flex gap-1">
103+
<div className="w-3 h-3 rounded-sm bg-gray-200 dark:bg-gray-800" />
104+
<div className="w-3 h-3 rounded-sm bg-primary/25" />
105+
<div className="w-3 h-3 rounded-sm bg-primary/50" />
106+
<div className="w-3 h-3 rounded-sm bg-primary/75" />
107+
<div className="w-3 h-3 rounded-sm bg-primary" />
108+
</div>
109+
<span>More</span>
110+
</div>
111+
</div>
112+
113+
<div className="flex gap-2">
114+
{/* Day labels */}
115+
<div className="flex flex-col gap-[2px] pr-1">
116+
<div className="h-3" /> {/* Spacer for month labels */}
117+
{dayLabels.map((day, index) => (
118+
<div key={index} className="h-3 text-[10px] text-muted-foreground flex items-center">
119+
{index % 2 === 1 ? day : ''}
120+
</div>
121+
))}
122+
</div>
123+
124+
<div className="flex-1">
125+
{/* Month labels */}
126+
<div className="flex h-3 mb-1 relative">
127+
{monthLabels.map(({ month, position }) => (
128+
<div
129+
key={`${month}-${position}`}
130+
className="absolute text-[10px] text-muted-foreground"
131+
style={{ left: `${(position / weeks) * 100}%` }}
132+
>
133+
{month}
134+
</div>
135+
))}
136+
</div>
137+
138+
{/* Activity grid */}
139+
<div className="flex gap-[2px]">
140+
{activityData.weeksData.map((week, weekIndex) => (
141+
<div key={weekIndex} className="flex flex-col gap-[2px]">
142+
{week.map((count, dayIndex) => {
143+
const date = new Date();
144+
date.setDate(date.getDate() - ((weeks - weekIndex - 1) * 7) - (6 - dayIndex));
145+
const dateStr = date.toLocaleDateString('en-US', {
146+
month: 'short',
147+
day: 'numeric',
148+
year: 'numeric'
149+
});
150+
151+
return (
152+
<div
153+
key={dayIndex}
154+
className={cn(
155+
"w-3 h-3 rounded-sm transition-colors cursor-pointer",
156+
getIntensityClass(count, activityData.maxCount)
157+
)}
158+
title={`${count} transcription${count !== 1 ? 's' : ''} on ${dateStr}`}
159+
/>
160+
);
161+
})}
162+
</div>
163+
))}
164+
</div>
165+
</div>
166+
</div>
167+
168+
<div className="text-xs text-muted-foreground">
169+
{history.length} total transcriptions in the last {weeks} weeks
170+
</div>
171+
</div>
172+
);
173+
}

src/components/AppContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface ErrorEventPayload {
2727

2828
export function AppContainer() {
2929
const { registerEvent } = useEventCoordinator("main");
30-
const [activeSection, setActiveSection] = useState<string>("recordings");
30+
const [activeSection, setActiveSection] = useState<string>("overview");
3131
const [showOnboarding, setShowOnboarding] = useState(false);
3232
const { settings, refreshSettings } = useSettings();
3333
const { checkAccessibilityPermission, checkMicrophonePermission } = useReadiness();
@@ -191,4 +191,4 @@ export function AppContainer() {
191191
</SidebarInset>
192192
</SidebarProvider>
193193
);
194-
}
194+
}

0 commit comments

Comments
 (0)