Skip to content

Commit ecacf02

Browse files
Copexitclaude
andcommitted
feat: floating tip toast for improved donation visibility
Slides in from bottom-right after random 20-40s delay. Expandable to reveal Lightning QR code and address. Session-dismissable, respects inline TipJar dismiss state to avoid double-prompting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a64769d commit ecacf02

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SponsorCta } from "@/components/SponsorCta";
2323
import { useKeyboardNav } from "@/hooks/useKeyboardNav";
2424
import { useDevMode } from "@/hooks/useDevMode";
2525
import { DevPanel } from "@/components/DevPanel";
26+
import { TipToast } from "@/components/TipToast";
2627

2728
type FixMethod = "RBF" | "CPFP";
2829

@@ -417,6 +418,7 @@ export default function Home() {
417418
)}
418419
</AnimatePresence>
419420

421+
<TipToast />
420422
<DevPanel
421423
visible={devMode.panelVisible}
422424
scenarios={devMode.scenarios}

src/components/TipToast.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { QRCodeSVG } from "qrcode.react";
5+
import { motion, AnimatePresence } from "motion/react";
6+
import { Heart, X } from "lucide-react";
7+
import { CopyButton } from "./ui/CopyButton";
8+
9+
const LN_ADDRESS = "woozycuticle72@walletofsatoshi.com";
10+
const DISMISS_KEY = "txfix-tip-toast-dismissed";
11+
const INLINE_DISMISS_KEY = "txfix-tip-dismissed";
12+
13+
function isDismissed(): boolean {
14+
try {
15+
return (
16+
sessionStorage.getItem(DISMISS_KEY) === "1" ||
17+
sessionStorage.getItem(INLINE_DISMISS_KEY) === "1"
18+
);
19+
} catch {
20+
return false;
21+
}
22+
}
23+
24+
function persistDismiss(): void {
25+
try {
26+
sessionStorage.setItem(DISMISS_KEY, "1");
27+
} catch {}
28+
}
29+
30+
export function TipToast() {
31+
const [visible, setVisible] = useState(false);
32+
const [dismissed, setDismissed] = useState(isDismissed);
33+
const [expanded, setExpanded] = useState(false);
34+
35+
useEffect(() => {
36+
if (dismissed) return;
37+
38+
const delay = Math.floor(Math.random() * 20000) + 20000; // 20-40s
39+
const timer = setTimeout(() => setVisible(true), delay);
40+
return () => clearTimeout(timer);
41+
}, [dismissed]);
42+
43+
const handleDismiss = (e: React.MouseEvent) => {
44+
e.stopPropagation();
45+
persistDismiss();
46+
setDismissed(true);
47+
};
48+
49+
return (
50+
<AnimatePresence>
51+
{visible && !dismissed && (
52+
<motion.div
53+
initial={{ opacity: 0, y: 20 }}
54+
animate={{ opacity: 1, y: 0 }}
55+
exit={{ opacity: 0, y: 20 }}
56+
transition={{ duration: 0.3 }}
57+
className="fixed bottom-4 right-4 left-4 sm:left-auto max-w-sm z-50"
58+
>
59+
<div className="relative bg-surface-elevated border border-bitcoin/30 rounded-xl shadow-xl overflow-hidden">
60+
{/* Collapsed row */}
61+
<button
62+
onClick={() => setExpanded(!expanded)}
63+
className="w-full flex items-center gap-3 px-4 py-3 text-left cursor-pointer group"
64+
>
65+
<Heart
66+
size={16}
67+
className="text-bitcoin shrink-0 group-hover:text-bitcoin/80 transition-colors"
68+
/>
69+
<span className="text-sm text-muted group-hover:text-foreground transition-colors flex-1">
70+
This tool is free and open source. Tip to keep it running.
71+
</span>
72+
</button>
73+
74+
{/* Dismiss */}
75+
<button
76+
onClick={handleDismiss}
77+
className="absolute top-3 right-3 text-muted/50 hover:text-foreground transition-colors cursor-pointer p-0.5"
78+
aria-label="Dismiss"
79+
>
80+
<X size={12} />
81+
</button>
82+
83+
{/* Expanded: QR + address */}
84+
<AnimatePresence>
85+
{expanded && (
86+
<motion.div
87+
initial={{ height: 0, opacity: 0 }}
88+
animate={{ height: "auto", opacity: 1 }}
89+
exit={{ height: 0, opacity: 0 }}
90+
transition={{ duration: 0.25 }}
91+
className="overflow-hidden"
92+
>
93+
<div className="px-4 pb-4 space-y-3">
94+
<div className="border-t border-card-border pt-3" />
95+
96+
<div className="flex justify-center">
97+
<div className="bg-white rounded-lg p-3">
98+
<QRCodeSVG
99+
value={`lightning:${LN_ADDRESS}`}
100+
size={140}
101+
level="M"
102+
includeMargin={false}
103+
/>
104+
</div>
105+
</div>
106+
107+
<div className="text-center space-y-2">
108+
<p className="text-xs text-muted">
109+
Scan with any Lightning wallet, or copy the address below
110+
</p>
111+
<div className="flex items-center justify-center gap-2">
112+
<code className="text-xs text-bitcoin bg-bitcoin/10 px-2 py-1 rounded font-mono break-all">
113+
{LN_ADDRESS}
114+
</code>
115+
<CopyButton
116+
text={LN_ADDRESS}
117+
label="Copy"
118+
className="text-[10px] px-2 py-0.5"
119+
/>
120+
</div>
121+
</div>
122+
</div>
123+
</motion.div>
124+
)}
125+
</AnimatePresence>
126+
</div>
127+
</motion.div>
128+
)}
129+
</AnimatePresence>
130+
);
131+
}

0 commit comments

Comments
 (0)