Skip to content

Commit c3f818f

Browse files
committed
feat: improve UI/UX for support links and Quick Tips section
- Fix external link handling in HelpSection using Tauri's open API - Add email client selector modal (Gmail vs default app) - Update Twitter references to X with profile link - Redesign Quick Tips in Overview to match Formatting's polished style - Add loading state for ShareStatsModal image copy operation - Remove unused Dashboard.tsx component (replaced by OverviewTab) Improves user experience with better visual feedback and consistent UI design
1 parent e67dc68 commit c3f818f

File tree

4 files changed

+155
-392
lines changed

4 files changed

+155
-392
lines changed

src/components/ShareStatsModal.tsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { useEffect, useRef, useState } from "react";
1+
import { Button } from "@/components/ui/button";
22
import {
33
Dialog,
44
DialogContent,
55
DialogHeader,
66
DialogTitle,
77
} from "@/components/ui/dialog";
8-
import { Button } from "@/components/ui/button";
9-
import { Copy, Check, Loader2, Download } from "lucide-react";
108
import { cn } from "@/lib/utils";
119
import { invoke } from "@tauri-apps/api/core";
10+
import { Check, Copy, Download, Loader2 } from "lucide-react";
11+
import { useEffect, useRef, useState } from "react";
1212
import { toast } from "sonner";
1313

1414
interface ShareStatsModalProps {
@@ -31,6 +31,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
3131
const [copied, setCopied] = useState(false);
3232
const [imageDataUrl, setImageDataUrl] = useState<string>("");
3333
const [isLoading, setIsLoading] = useState(true);
34+
const [isCopying, setIsCopying] = useState(false);
3435

3536
useEffect(() => {
3637
if (open) {
@@ -68,7 +69,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
6869
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
6970
gradient.addColorStop(0, "#0f0f0f");
7071
gradient.addColorStop(1, "#1a1a1a");
71-
72+
7273
ctx.fillStyle = gradient;
7374
ctx.fillRect(0, 0, canvas.width, canvas.height);
7475

@@ -123,7 +124,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
123124
const col = index % 2;
124125
const x = gridStartX + col * (cardWidth + cardGap);
125126
const y = gridStartY + row * (cardHeight + cardGap);
126-
127+
127128
// Simple card background
128129
ctx.fillStyle = "rgba(255, 255, 255, 0.05)";
129130
ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
@@ -134,7 +135,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
134135
ctx.stroke();
135136

136137
// Card label - scaled up
137-
ctx.font = "40px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
138+
ctx.font = "48px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
138139
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
139140
ctx.textAlign = "center";
140141
ctx.fillText(card.label, x + cardWidth/2, y + 90);
@@ -145,22 +146,22 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
145146
ctx.fillText(card.value, x + cardWidth/2, y + 220);
146147

147148
// Card subtitle - scaled up
148-
ctx.font = "36px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
149+
ctx.font = "40px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
149150
ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
150151
ctx.fillText(card.subtitle, x + cardWidth/2, y + 290);
151152
});
152153

153-
// Current Streak at the bottom - scaled up
154-
if (stats.currentStreak > 0) {
155-
ctx.font = "bold 80px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
156-
ctx.fillStyle = "#ffffff";
157-
ctx.textAlign = "center";
158-
const streakY = gridStartY + 2 * (cardHeight + cardGap) + 120;
159-
ctx.fillText(`🔥 ${stats.currentStreak} Day Streak`, canvas.width / 2, streakY);
160-
}
154+
// // Current Streak at the bottom - scaled up
155+
// if (stats.currentStreak > 0) {
156+
// ctx.font = "bold 80px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
157+
// ctx.fillStyle = "#ffffff";
158+
// ctx.textAlign = "center";
159+
// const streakY = gridStartY + 2 * (cardHeight + cardGap) + 120;
160+
// ctx.fillText(`🔥 ${stats.currentStreak} Day Streak`, canvas.width / 2, streakY);
161+
// }
161162

162163
// Website at the bottom - scaled up
163-
ctx.font = "48px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
164+
ctx.font = "56px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
164165
ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
165166
ctx.textAlign = "center";
166167
ctx.fillText("voicetypr.com", canvas.width / 2, canvas.height - 80);
@@ -171,20 +172,24 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
171172
};
172173

173174
const copyImageToClipboard = async () => {
174-
if (!canvasRef.current || !imageDataUrl) return;
175+
if (!canvasRef.current || !imageDataUrl || isCopying) return;
175176

177+
setIsCopying(true);
178+
176179
try {
177180
// Use Tauri's clipboard API for system-level copy
178-
await invoke("copy_image_to_clipboard", {
179-
imageDataUrl: imageDataUrl
181+
await invoke("copy_image_to_clipboard", {
182+
imageDataUrl: imageDataUrl
180183
});
181-
184+
182185
setCopied(true);
183186
toast.success("Stats image copied to clipboard!");
184187
setTimeout(() => setCopied(false), 2000);
185188
} catch (err) {
186189
console.error("Failed to copy image to clipboard:", err);
187190
toast.error("Failed to copy image. Try the download button instead.");
191+
} finally {
192+
setIsCopying(false);
188193
}
189194
};
190195

@@ -193,7 +198,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
193198

194199
try {
195200
const fileName = `voicetypr-stats-${Date.now()}.png`;
196-
201+
197202
// Use Tauri's save dialog to let user choose location
198203
const { save } = await import('@tauri-apps/plugin-dialog');
199204
const filePath = await save({
@@ -206,7 +211,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
206211

207212
if (filePath) {
208213
// Use the Rust backend to save the file (best practice)
209-
await invoke("save_image_to_file", {
214+
await invoke("save_image_to_file", {
210215
imageDataUrl: imageDataUrl,
211216
filePath: filePath
212217
});
@@ -229,7 +234,7 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
229234
<DialogHeader>
230235
<DialogTitle>Share Your Stats</DialogTitle>
231236
</DialogHeader>
232-
237+
233238
<div className="space-y-4">
234239
{/* Canvas Preview */}
235240
<div className="relative rounded-lg overflow-hidden bg-black/5 border border-border/50 min-h-[300px]">
@@ -252,12 +257,18 @@ export function ShareStatsModal({ open, onOpenChange, stats }: ShareStatsModalPr
252257
<div className="flex justify-center gap-2">
253258
<Button
254259
onClick={copyImageToClipboard}
260+
disabled={isCopying || !imageDataUrl}
255261
className={cn(
256-
"gap-2",
262+
"gap-2 min-w-[120px]",
257263
copied && "bg-green-600 hover:bg-green-600"
258264
)}
259265
>
260-
{copied ? (
266+
{isCopying ? (
267+
<>
268+
<Loader2 className="h-4 w-4 animate-spin" />
269+
Copying...
270+
</>
271+
) : copied ? (
261272
<>
262273
<Check className="h-4 w-4" />
263274
Copied!

0 commit comments

Comments
 (0)