mirror of
https://github.com/Such-Software/smirk-website.git
synced 2026-10-17 05:51:28 +02:00
fix chrome link and add OG
This commit is contained in:
@@ -40,3 +40,7 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# repo-to-text
|
||||||
|
.repo-to-text-settings.yaml
|
||||||
|
repo-to-text_*.txt
|
||||||
|
|||||||
+27
-1
@@ -7,9 +7,35 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const siteUrl = "https://smirk.cash";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
default: "Smirk Wallet",
|
||||||
|
template: "%s | Smirk Wallet",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Multi-coin crypto wallet browser extension. Send, receive, and tip BTC, LTC, XMR, WOW, and GRIN.",
|
||||||
|
metadataBase: new URL(siteUrl),
|
||||||
|
openGraph: {
|
||||||
title: "Smirk Wallet",
|
title: "Smirk Wallet",
|
||||||
description: "Sign in with your Smirk wallet",
|
description:
|
||||||
|
"Multi-coin crypto wallet browser extension. Send, receive, and tip BTC, LTC, XMR, WOW, and GRIN.",
|
||||||
|
url: siteUrl,
|
||||||
|
siteName: "Smirk Wallet",
|
||||||
|
type: "website",
|
||||||
|
locale: "en_US",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Smirk Wallet",
|
||||||
|
description:
|
||||||
|
"Multi-coin crypto wallet browser extension. Send, receive, and tip BTC, LTC, XMR, WOW, and GRIN.",
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: "/logo.svg",
|
||||||
|
apple: "/logo.svg",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ImageResponse } from "next/og";
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
||||||
|
export const alt = "Smirk Wallet";
|
||||||
|
export const size = { width: 1200, height: 630 };
|
||||||
|
export const contentType = "image/png";
|
||||||
|
|
||||||
|
export default async function Image() {
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "#0a0a0a",
|
||||||
|
gap: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Smirk emoji as placeholder for logo */}
|
||||||
|
<div style={{ fontSize: 120, display: "flex" }}>😏</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 64,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#fbeb0a",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Smirk Wallet
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 28,
|
||||||
|
color: "#a1a1aa",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
BTC • LTC • XMR • WOW • GRIN
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 22,
|
||||||
|
color: "#71717a",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Multi-coin crypto wallet & tipping extension
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ ...size }
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -202,7 +202,7 @@ export default function Home() {
|
|||||||
<p className="creepster-text text-xl text-[#fbeb0a]">GET SMIRKED</p>
|
<p className="creepster-text text-xl text-[#fbeb0a]">GET SMIRKED</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<a
|
<a
|
||||||
href="https://chrome.google.com/webstore/detail/smirk-wallet"
|
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg
|
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { getPublicTipInfo, type PublicTipInfo } from '@/lib/api';
|
||||||
|
|
||||||
|
const COINS: Record<string, { name: string; icon: string; divisor: number; decimals: number }> = {
|
||||||
|
btc: { name: 'Bitcoin', icon: '/coins/bitcoin.svg', divisor: 100_000_000, decimals: 8 },
|
||||||
|
ltc: { name: 'Litecoin', icon: '/coins/litecoin.svg', divisor: 100_000_000, decimals: 8 },
|
||||||
|
xmr: { name: 'Monero', icon: '/coins/monero.svg', divisor: 1_000_000_000_000, decimals: 12 },
|
||||||
|
wow: { name: 'Wownero', icon: '/coins/wownero.svg', divisor: 100_000_000_000, decimals: 11 },
|
||||||
|
grin: { name: 'Grin', icon: '/coins/grin.svg', divisor: 1_000_000_000, decimals: 9 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatAmount(amount: number, asset: string): string {
|
||||||
|
const coin = COINS[asset.toLowerCase()];
|
||||||
|
if (!coin) return `${amount} ${asset.toUpperCase()}`;
|
||||||
|
const value = amount / coin.divisor;
|
||||||
|
return `${value.toFixed(Math.min(coin.decimals, 8))} ${asset.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TipPageClient({ tipId }: { tipId: string }) {
|
||||||
|
const [tip, setTip] = useState<PublicTipInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [hasExtension, setHasExtension] = useState<boolean | null>(null);
|
||||||
|
const [fragmentKey, setFragmentKey] = useState<string | null>(null);
|
||||||
|
const [claiming, setClaiming] = useState(false);
|
||||||
|
const [claimError, setClaimError] = useState<string | null>(null);
|
||||||
|
const [claimSuccess, setClaimSuccess] = useState<{ txid: string } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for extension
|
||||||
|
const check = () => setHasExtension(!!window.smirk);
|
||||||
|
check();
|
||||||
|
const timeout = setTimeout(check, 500);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Extract fragment key from URL
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
if (hash && hash.length > 1) {
|
||||||
|
setFragmentKey(hash.slice(1)); // Remove the # prefix
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tipId) return;
|
||||||
|
|
||||||
|
async function loadTip() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const tipInfo = await getPublicTipInfo(tipId);
|
||||||
|
setTip(tipInfo);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load tip');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTip();
|
||||||
|
}, [tipId]);
|
||||||
|
|
||||||
|
const handleClaim = async () => {
|
||||||
|
if (!window.smirk || !fragmentKey || !tipId) return;
|
||||||
|
|
||||||
|
setClaiming(true);
|
||||||
|
setClaimError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First connect to extension (if not already)
|
||||||
|
await window.smirk.connect();
|
||||||
|
|
||||||
|
// Claim the tip
|
||||||
|
const result = await window.smirk.claimPublicTip(tipId, fragmentKey);
|
||||||
|
|
||||||
|
if (result.success && result.txid) {
|
||||||
|
setClaimSuccess({ txid: result.txid });
|
||||||
|
// Refresh tip info to show claimed status
|
||||||
|
const tipInfo = await getPublicTipInfo(tipId);
|
||||||
|
setTip(tipInfo);
|
||||||
|
} else {
|
||||||
|
setClaimError(result.error || 'Failed to claim tip');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setClaimError(err instanceof Error ? err.message : 'Failed to claim tip');
|
||||||
|
} finally {
|
||||||
|
setClaiming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isClaimable = tip?.status === 'pending' &&
|
||||||
|
tip.is_public &&
|
||||||
|
fragmentKey &&
|
||||||
|
(tip.funding_confirmations ?? 0) >= (tip.confirmations_required ?? 0);
|
||||||
|
|
||||||
|
const needsConfirmations = tip?.status === 'pending' &&
|
||||||
|
tip.is_public &&
|
||||||
|
fragmentKey &&
|
||||||
|
(tip.funding_confirmations ?? 0) < (tip.confirmations_required ?? 0);
|
||||||
|
|
||||||
|
const coin = tip ? COINS[tip.asset.toLowerCase()] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="mb-8 flex flex-col items-center">
|
||||||
|
<Link href="/" className="flex flex-col items-center">
|
||||||
|
<h1 className="creepster-text text-3xl mb-2">SMIRK WALLET</h1>
|
||||||
|
<div className="logo-glow">
|
||||||
|
<Image
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Smirk"
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading state */}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
|
||||||
|
<p className="text-zinc-400">Loading tip...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error state */}
|
||||||
|
{error && !loading && (
|
||||||
|
<div className="flex flex-col items-center gap-6 text-center">
|
||||||
|
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-3xl">!</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Tip Not Found</h2>
|
||||||
|
<p className="text-zinc-400 text-sm max-w-md">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tip found */}
|
||||||
|
{tip && !loading && (
|
||||||
|
<div className="flex flex-col items-center gap-6 text-center max-w-md">
|
||||||
|
{/* Status indicator */}
|
||||||
|
{tip.status === 'pending' ? (
|
||||||
|
<div className="w-16 h-16 bg-[#fbeb0a] rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-3xl">🎁</span>
|
||||||
|
</div>
|
||||||
|
) : tip.status === 'claimed' ? (
|
||||||
|
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 text-green-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-16 h-16 bg-zinc-700 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-3xl text-zinc-400">X</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tip info */}
|
||||||
|
<div className="bg-zinc-900 rounded-xl p-6 w-full">
|
||||||
|
{coin && (
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-4">
|
||||||
|
<Image src={coin.icon} alt={coin.name} width={48} height={48} />
|
||||||
|
<span className="text-2xl font-bold">{formatAmount(tip.amount, tip.asset)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-sm text-zinc-400 space-y-1">
|
||||||
|
<p>Status: <span className={
|
||||||
|
tip.status === 'pending' ? 'text-[#fbeb0a]' :
|
||||||
|
tip.status === 'claimed' ? 'text-green-400' :
|
||||||
|
'text-zinc-500'
|
||||||
|
}>{tip.status}</span></p>
|
||||||
|
<p>Created: {new Date(tip.created_at).toLocaleDateString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Claim success */}
|
||||||
|
{claimSuccess && (
|
||||||
|
<div className="bg-green-500/10 border border-green-500/50 rounded-xl p-4">
|
||||||
|
<h3 className="text-green-400 font-semibold mb-2">Claimed!</h3>
|
||||||
|
<p className="text-zinc-400 text-sm">
|
||||||
|
Funds are being sent to your wallet.
|
||||||
|
</p>
|
||||||
|
<p className="text-zinc-500 text-xs mt-2 font-mono break-all">
|
||||||
|
TX: {claimSuccess.txid}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Needs confirmations */}
|
||||||
|
{needsConfirmations && !claimSuccess && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold text-[#fbeb0a]">Waiting for Confirmations</h2>
|
||||||
|
<div className="text-zinc-400 text-sm">
|
||||||
|
<p>This tip is still confirming on the blockchain.</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
<span className="text-[#fbeb0a] font-semibold">{tip?.funding_confirmations ?? 0}</span>
|
||||||
|
{' / '}
|
||||||
|
<span>{tip?.confirmations_required ?? 0}</span>
|
||||||
|
{' confirmations'}
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-zinc-500 text-xs">
|
||||||
|
Refresh this page to check progress.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Claim button for public tips */}
|
||||||
|
{isClaimable && !claimSuccess && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
{hasExtension ? 'Ready to Claim!' : 'Install Smirk Wallet'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{hasExtension ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={handleClaim}
|
||||||
|
disabled={claiming}
|
||||||
|
className="px-8 py-4 bg-[#fbeb0a] text-black font-bold rounded-xl
|
||||||
|
hover:bg-[#d4c708] transition-colors disabled:opacity-50
|
||||||
|
disabled:cursor-not-allowed text-lg w-full"
|
||||||
|
>
|
||||||
|
{claiming ? 'Claiming...' : 'Claim This Tip'}
|
||||||
|
</button>
|
||||||
|
{claimError && (
|
||||||
|
<p className="text-red-400 text-sm">{claimError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-zinc-400 text-sm space-y-4">
|
||||||
|
<p>You need the Smirk Wallet extension to claim tips.</p>
|
||||||
|
<a
|
||||||
|
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-block px-6 py-3 bg-[#fbeb0a] text-black font-bold rounded-xl
|
||||||
|
hover:bg-[#d4c708] transition-colors"
|
||||||
|
>
|
||||||
|
Install Smirk Wallet
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Targeted tip instructions (no fragment key) */}
|
||||||
|
{tip.status === 'pending' && !fragmentKey && !tip.is_public && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
{hasExtension ? 'Claim This Tip' : 'Install Smirk Wallet'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{hasExtension ? (
|
||||||
|
<div className="text-zinc-400 text-sm space-y-2">
|
||||||
|
<p>To claim this tip:</p>
|
||||||
|
<ol className="text-left list-decimal list-inside space-y-1">
|
||||||
|
<li>Click the Smirk extension icon</li>
|
||||||
|
<li>Go to <strong>Inbox</strong></li>
|
||||||
|
<li>Click <strong>Claim</strong> on this tip</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-zinc-400 text-sm space-y-2">
|
||||||
|
<p>You need the Smirk Wallet extension to claim tips.</p>
|
||||||
|
<a
|
||||||
|
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-block px-6 py-3 bg-[#fbeb0a] text-black font-bold rounded-xl
|
||||||
|
hover:bg-[#d4c708] transition-colors"
|
||||||
|
>
|
||||||
|
Install Smirk Wallet
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Public tip with missing fragment key */}
|
||||||
|
{tip.status === 'pending' && tip.is_public && !fragmentKey && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold text-red-400">Incomplete Link</h2>
|
||||||
|
<div className="text-zinc-400 text-sm space-y-2">
|
||||||
|
<p>This is a public tip, but the claim link is incomplete.</p>
|
||||||
|
<p>Make sure you have the <strong>full URL</strong> including the secret key after the <code className="bg-zinc-800 px-1 rounded">#</code> symbol.</p>
|
||||||
|
<p className="text-zinc-500 text-xs mt-4">
|
||||||
|
Example: smirk.cash/tip/abc123<strong>#secretKey</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tip.status === 'claimed' && !claimSuccess && (
|
||||||
|
<p className="text-green-400">This tip has already been claimed.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tip.status !== 'pending' && tip.status !== 'claimed' && (
|
||||||
|
<p className="text-zinc-500">This tip is no longer available.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+43
-331
@@ -1,339 +1,51 @@
|
|||||||
'use client';
|
import type { Metadata } from 'next';
|
||||||
|
import { getPublicTipInfo } from '@/lib/api';
|
||||||
|
import TipPageClient, { formatAmount } from './TipPageClient';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
const COIN_NAMES: Record<string, string> = {
|
||||||
import { useParams } from 'next/navigation';
|
btc: 'Bitcoin',
|
||||||
import Image from 'next/image';
|
ltc: 'Litecoin',
|
||||||
import Link from 'next/link';
|
xmr: 'Monero',
|
||||||
import { getPublicTipInfo, type PublicTipInfo } from '@/lib/api';
|
wow: 'Wownero',
|
||||||
|
grin: 'Grin',
|
||||||
const COINS: Record<string, { name: string; icon: string; divisor: number; decimals: number }> = {
|
|
||||||
btc: { name: 'Bitcoin', icon: '/coins/bitcoin.svg', divisor: 100_000_000, decimals: 8 },
|
|
||||||
ltc: { name: 'Litecoin', icon: '/coins/litecoin.svg', divisor: 100_000_000, decimals: 8 },
|
|
||||||
xmr: { name: 'Monero', icon: '/coins/monero.svg', divisor: 1_000_000_000_000, decimals: 12 },
|
|
||||||
wow: { name: 'Wownero', icon: '/coins/wownero.svg', divisor: 100_000_000_000, decimals: 11 },
|
|
||||||
grin: { name: 'Grin', icon: '/coins/grin.svg', divisor: 1_000_000_000, decimals: 9 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatAmount(amount: number, asset: string): string {
|
type Props = {
|
||||||
const coin = COINS[asset.toLowerCase()];
|
params: Promise<{ id: string }>;
|
||||||
if (!coin) return `${amount} ${asset.toUpperCase()}`;
|
|
||||||
const value = amount / coin.divisor;
|
|
||||||
return `${value.toFixed(Math.min(coin.decimals, 8))} ${asset.toUpperCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TipPage() {
|
|
||||||
const params = useParams();
|
|
||||||
const tipId = params.id as string;
|
|
||||||
|
|
||||||
const [tip, setTip] = useState<PublicTipInfo | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [hasExtension, setHasExtension] = useState<boolean | null>(null);
|
|
||||||
const [fragmentKey, setFragmentKey] = useState<string | null>(null);
|
|
||||||
const [claiming, setClaiming] = useState(false);
|
|
||||||
const [claimError, setClaimError] = useState<string | null>(null);
|
|
||||||
const [claimSuccess, setClaimSuccess] = useState<{ txid: string } | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check for extension
|
|
||||||
const check = () => setHasExtension(!!window.smirk);
|
|
||||||
check();
|
|
||||||
const timeout = setTimeout(check, 500);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Extract fragment key from URL
|
|
||||||
useEffect(() => {
|
|
||||||
const hash = window.location.hash;
|
|
||||||
if (hash && hash.length > 1) {
|
|
||||||
setFragmentKey(hash.slice(1)); // Remove the # prefix
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!tipId) return;
|
|
||||||
|
|
||||||
async function loadTip() {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
const tipInfo = await getPublicTipInfo(tipId);
|
|
||||||
setTip(tipInfo);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load tip');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTip();
|
|
||||||
}, [tipId]);
|
|
||||||
|
|
||||||
const handleClaim = async () => {
|
|
||||||
if (!window.smirk || !fragmentKey || !tipId) return;
|
|
||||||
|
|
||||||
setClaiming(true);
|
|
||||||
setClaimError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First connect to extension (if not already)
|
|
||||||
await window.smirk.connect();
|
|
||||||
|
|
||||||
// Claim the tip
|
|
||||||
const result = await window.smirk.claimPublicTip(tipId, fragmentKey);
|
|
||||||
|
|
||||||
if (result.success && result.txid) {
|
|
||||||
setClaimSuccess({ txid: result.txid });
|
|
||||||
// Refresh tip info to show claimed status
|
|
||||||
const tipInfo = await getPublicTipInfo(tipId);
|
|
||||||
setTip(tipInfo);
|
|
||||||
} else {
|
|
||||||
setClaimError(result.error || 'Failed to claim tip');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setClaimError(err instanceof Error ? err.message : 'Failed to claim tip');
|
|
||||||
} finally {
|
|
||||||
setClaiming(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isClaimable = tip?.status === 'pending' &&
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
tip.is_public &&
|
const { id } = await params;
|
||||||
fragmentKey &&
|
|
||||||
(tip.funding_confirmations ?? 0) >= (tip.confirmations_required ?? 0);
|
|
||||||
|
|
||||||
const needsConfirmations = tip?.status === 'pending' &&
|
try {
|
||||||
tip.is_public &&
|
const tip = await getPublicTipInfo(id);
|
||||||
fragmentKey &&
|
const coinName = COIN_NAMES[tip.asset.toLowerCase()] || tip.asset.toUpperCase();
|
||||||
(tip.funding_confirmations ?? 0) < (tip.confirmations_required ?? 0);
|
const amount = formatAmount(tip.amount, tip.asset);
|
||||||
|
const statusText = tip.status === 'pending' ? 'Claim it now!' : `Status: ${tip.status}`;
|
||||||
|
|
||||||
const coin = tip ? COINS[tip.asset.toLowerCase()] : null;
|
return {
|
||||||
|
title: `${amount} ${coinName} Tip`,
|
||||||
return (
|
description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`,
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
openGraph: {
|
||||||
{/* Logo */}
|
title: `${amount} ${coinName} Tip`,
|
||||||
<div className="mb-8 flex flex-col items-center">
|
description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`,
|
||||||
<Link href="/" className="flex flex-col items-center">
|
type: 'website',
|
||||||
<h1 className="creepster-text text-3xl mb-2">SMIRK WALLET</h1>
|
},
|
||||||
<div className="logo-glow">
|
twitter: {
|
||||||
<Image
|
card: 'summary_large_image',
|
||||||
src="/logo.svg"
|
title: `${amount} ${coinName} Tip`,
|
||||||
alt="Smirk"
|
description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`,
|
||||||
width={100}
|
},
|
||||||
height={100}
|
};
|
||||||
priority
|
} catch {
|
||||||
/>
|
return {
|
||||||
</div>
|
title: 'Crypto Tip',
|
||||||
</Link>
|
description: 'Claim your crypto tip with Smirk Wallet.',
|
||||||
</div>
|
};
|
||||||
|
}
|
||||||
{/* Loading state */}
|
}
|
||||||
{loading && (
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
export default async function TipPage({ params }: Props) {
|
||||||
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
|
const { id } = await params;
|
||||||
<p className="text-zinc-400">Loading tip...</p>
|
return <TipPageClient tipId={id} />;
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error state */}
|
|
||||||
{error && !loading && (
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center">
|
|
||||||
<span className="text-3xl">!</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold mb-2">Tip Not Found</h2>
|
|
||||||
<p className="text-zinc-400 text-sm max-w-md">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Go Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tip found */}
|
|
||||||
{tip && !loading && (
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center max-w-md">
|
|
||||||
{/* Status indicator */}
|
|
||||||
{tip.status === 'pending' ? (
|
|
||||||
<div className="w-16 h-16 bg-[#fbeb0a] rounded-full flex items-center justify-center">
|
|
||||||
<span className="text-3xl">🎁</span>
|
|
||||||
</div>
|
|
||||||
) : tip.status === 'claimed' ? (
|
|
||||||
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center">
|
|
||||||
<svg
|
|
||||||
className="w-8 h-8 text-green-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-16 h-16 bg-zinc-700 rounded-full flex items-center justify-center">
|
|
||||||
<span className="text-3xl text-zinc-400">X</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tip info */}
|
|
||||||
<div className="bg-zinc-900 rounded-xl p-6 w-full">
|
|
||||||
{coin && (
|
|
||||||
<div className="flex items-center justify-center gap-3 mb-4">
|
|
||||||
<Image src={coin.icon} alt={coin.name} width={48} height={48} />
|
|
||||||
<span className="text-2xl font-bold">{formatAmount(tip.amount, tip.asset)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-sm text-zinc-400 space-y-1">
|
|
||||||
<p>Status: <span className={
|
|
||||||
tip.status === 'pending' ? 'text-[#fbeb0a]' :
|
|
||||||
tip.status === 'claimed' ? 'text-green-400' :
|
|
||||||
'text-zinc-500'
|
|
||||||
}>{tip.status}</span></p>
|
|
||||||
<p>Created: {new Date(tip.created_at).toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Claim success */}
|
|
||||||
{claimSuccess && (
|
|
||||||
<div className="bg-green-500/10 border border-green-500/50 rounded-xl p-4">
|
|
||||||
<h3 className="text-green-400 font-semibold mb-2">Claimed!</h3>
|
|
||||||
<p className="text-zinc-400 text-sm">
|
|
||||||
Funds are being sent to your wallet.
|
|
||||||
</p>
|
|
||||||
<p className="text-zinc-500 text-xs mt-2 font-mono break-all">
|
|
||||||
TX: {claimSuccess.txid}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Needs confirmations */}
|
|
||||||
{needsConfirmations && !claimSuccess && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-xl font-semibold text-[#fbeb0a]">Waiting for Confirmations</h2>
|
|
||||||
<div className="text-zinc-400 text-sm">
|
|
||||||
<p>This tip is still confirming on the blockchain.</p>
|
|
||||||
<p className="mt-2">
|
|
||||||
<span className="text-[#fbeb0a] font-semibold">{tip?.funding_confirmations ?? 0}</span>
|
|
||||||
{' / '}
|
|
||||||
<span>{tip?.confirmations_required ?? 0}</span>
|
|
||||||
{' confirmations'}
|
|
||||||
</p>
|
|
||||||
<p className="mt-3 text-zinc-500 text-xs">
|
|
||||||
Refresh this page to check progress.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Claim button for public tips */}
|
|
||||||
{isClaimable && !claimSuccess && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
{hasExtension ? 'Ready to Claim!' : 'Install Smirk Wallet'}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{hasExtension ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<button
|
|
||||||
onClick={handleClaim}
|
|
||||||
disabled={claiming}
|
|
||||||
className="px-8 py-4 bg-[#fbeb0a] text-black font-bold rounded-xl
|
|
||||||
hover:bg-[#d4c708] transition-colors disabled:opacity-50
|
|
||||||
disabled:cursor-not-allowed text-lg w-full"
|
|
||||||
>
|
|
||||||
{claiming ? 'Claiming...' : 'Claim This Tip'}
|
|
||||||
</button>
|
|
||||||
{claimError && (
|
|
||||||
<p className="text-red-400 text-sm">{claimError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-zinc-400 text-sm space-y-4">
|
|
||||||
<p>You need the Smirk Wallet extension to claim tips.</p>
|
|
||||||
<a
|
|
||||||
href="https://chrome.google.com/webstore/detail/smirk-wallet"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-block px-6 py-3 bg-[#fbeb0a] text-black font-bold rounded-xl
|
|
||||||
hover:bg-[#d4c708] transition-colors"
|
|
||||||
>
|
|
||||||
Install Smirk Wallet
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Targeted tip instructions (no fragment key) */}
|
|
||||||
{tip.status === 'pending' && !fragmentKey && !tip.is_public && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
{hasExtension ? 'Claim This Tip' : 'Install Smirk Wallet'}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{hasExtension ? (
|
|
||||||
<div className="text-zinc-400 text-sm space-y-2">
|
|
||||||
<p>To claim this tip:</p>
|
|
||||||
<ol className="text-left list-decimal list-inside space-y-1">
|
|
||||||
<li>Click the Smirk extension icon</li>
|
|
||||||
<li>Go to <strong>Inbox</strong></li>
|
|
||||||
<li>Click <strong>Claim</strong> on this tip</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-zinc-400 text-sm space-y-2">
|
|
||||||
<p>You need the Smirk Wallet extension to claim tips.</p>
|
|
||||||
<a
|
|
||||||
href="https://chrome.google.com/webstore/detail/smirk-wallet"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-block px-6 py-3 bg-[#fbeb0a] text-black font-bold rounded-xl
|
|
||||||
hover:bg-[#d4c708] transition-colors"
|
|
||||||
>
|
|
||||||
Install Smirk Wallet
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Public tip with missing fragment key */}
|
|
||||||
{tip.status === 'pending' && tip.is_public && !fragmentKey && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-xl font-semibold text-red-400">Incomplete Link</h2>
|
|
||||||
<div className="text-zinc-400 text-sm space-y-2">
|
|
||||||
<p>This is a public tip, but the claim link is incomplete.</p>
|
|
||||||
<p>Make sure you have the <strong>full URL</strong> including the secret key after the <code className="bg-zinc-800 px-1 rounded">#</code> symbol.</p>
|
|
||||||
<p className="text-zinc-500 text-xs mt-4">
|
|
||||||
Example: smirk.cash/tip/abc123<strong>#secretKey</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip.status === 'claimed' && !claimSuccess && (
|
|
||||||
<p className="text-green-400">This tip has already been claimed.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip.status !== 'pending' && tip.status !== 'claimed' && (
|
|
||||||
<p className="text-zinc-500">This tip is no longer available.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Go Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export default function TipsPage() {
|
|||||||
<p className="text-yellow-400 text-sm">
|
<p className="text-yellow-400 text-sm">
|
||||||
Install the{' '}
|
Install the{' '}
|
||||||
<a
|
<a
|
||||||
href="https://chromewebstore.google.com/detail/smirk/placeholder"
|
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline"
|
className="underline"
|
||||||
|
|||||||
Reference in New Issue
Block a user