diff --git a/.gitignore b/.gitignore index b721bff..c7fb275 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# repo-to-text +.repo-to-text-settings.yaml +repo-to-text_*.txt diff --git a/src/app/layout.tsx b/src/app/layout.tsx index faeba2f..960a455 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,9 +7,35 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); +const siteUrl = "https://smirk.cash"; + export const metadata: Metadata = { - title: "Smirk Wallet", - description: "Sign in with your Smirk wallet", + 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", + 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({ diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx new file mode 100644 index 0000000..8a128af --- /dev/null +++ b/src/app/opengraph-image.tsx @@ -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( + ( +
+ {/* Smirk emoji as placeholder for logo */} +
😏
+
+ Smirk Wallet +
+
+ BTC • LTC • XMR • WOW • GRIN +
+
+ Multi-coin crypto wallet & tipping extension +
+
+ ), + { ...size } + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index ecc17ca..1585dfc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -202,7 +202,7 @@ export default function Home() {

GET SMIRKED

+ {/* Logo */} +
+ +

SMIRK WALLET

+
+ Smirk +
+ +
+ + {/* Loading state */} + {loading && ( +
+
+

Loading tip...

+
+ )} + + {/* Error state */} + {error && !loading && ( +
+
+ ! +
+
+

Tip Not Found

+

+ {error} +

+
+ + Go Home + +
+ )} + + {/* Tip found */} + {tip && !loading && ( +
+ {/* Status indicator */} + {tip.status === 'pending' ? ( +
+ 🎁 +
+ ) : tip.status === 'claimed' ? ( +
+ + + +
+ ) : ( +
+ X +
+ )} + + {/* Tip info */} +
+ {coin && ( +
+ {coin.name} + {formatAmount(tip.amount, tip.asset)} +
+ )} + +
+

Status: {tip.status}

+

Created: {new Date(tip.created_at).toLocaleDateString()}

+
+
+ + {/* Claim success */} + {claimSuccess && ( +
+

Claimed!

+

+ Funds are being sent to your wallet. +

+

+ TX: {claimSuccess.txid} +

+
+ )} + + {/* Needs confirmations */} + {needsConfirmations && !claimSuccess && ( +
+

Waiting for Confirmations

+
+

This tip is still confirming on the blockchain.

+

+ {tip?.funding_confirmations ?? 0} + {' / '} + {tip?.confirmations_required ?? 0} + {' confirmations'} +

+

+ Refresh this page to check progress. +

+
+
+ )} + + {/* Claim button for public tips */} + {isClaimable && !claimSuccess && ( +
+

+ {hasExtension ? 'Ready to Claim!' : 'Install Smirk Wallet'} +

+ + {hasExtension ? ( +
+ + {claimError && ( +

{claimError}

+ )} +
+ ) : ( +
+

You need the Smirk Wallet extension to claim tips.

+
+ Install Smirk Wallet + +
+ )} +
+ )} + + {/* Targeted tip instructions (no fragment key) */} + {tip.status === 'pending' && !fragmentKey && !tip.is_public && ( +
+

+ {hasExtension ? 'Claim This Tip' : 'Install Smirk Wallet'} +

+ + {hasExtension ? ( +
+

To claim this tip:

+
    +
  1. Click the Smirk extension icon
  2. +
  3. Go to Inbox
  4. +
  5. Click Claim on this tip
  6. +
+
+ ) : ( +
+

You need the Smirk Wallet extension to claim tips.

+ + Install Smirk Wallet + +
+ )} +
+ )} + + {/* Public tip with missing fragment key */} + {tip.status === 'pending' && tip.is_public && !fragmentKey && ( +
+

Incomplete Link

+
+

This is a public tip, but the claim link is incomplete.

+

Make sure you have the full URL including the secret key after the # symbol.

+

+ Example: smirk.cash/tip/abc123#secretKey +

+
+
+ )} + + {tip.status === 'claimed' && !claimSuccess && ( +

This tip has already been claimed.

+ )} + + {tip.status !== 'pending' && tip.status !== 'claimed' && ( +

This tip is no longer available.

+ )} + + + Go Home + +
+ )} +
+ ); +} diff --git a/src/app/tip/[id]/page.tsx b/src/app/tip/[id]/page.tsx index 649ec1b..655fd9c 100644 --- a/src/app/tip/[id]/page.tsx +++ b/src/app/tip/[id]/page.tsx @@ -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'; -import { useParams } from 'next/navigation'; -import Image from 'next/image'; -import Link from 'next/link'; -import { getPublicTipInfo, type PublicTipInfo } from '@/lib/api'; - -const COINS: Record = { - 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 }, +const COIN_NAMES: Record = { + btc: 'Bitcoin', + ltc: 'Litecoin', + xmr: 'Monero', + wow: 'Wownero', + grin: 'Grin', }; -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()}`; +type Props = { + params: Promise<{ id: string }>; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + + try { + const tip = await getPublicTipInfo(id); + const coinName = COIN_NAMES[tip.asset.toLowerCase()] || tip.asset.toUpperCase(); + const amount = formatAmount(tip.amount, tip.asset); + const statusText = tip.status === 'pending' ? 'Claim it now!' : `Status: ${tip.status}`; + + return { + title: `${amount} ${coinName} Tip`, + description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`, + openGraph: { + title: `${amount} ${coinName} Tip`, + description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`, + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: `${amount} ${coinName} Tip`, + description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`, + }, + }; + } catch { + return { + title: 'Crypto Tip', + description: 'Claim your crypto tip with Smirk Wallet.', + }; + } } -export default function TipPage() { - const params = useParams(); - const tipId = params.id as string; - - const [tip, setTip] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [hasExtension, setHasExtension] = useState(null); - const [fragmentKey, setFragmentKey] = useState(null); - const [claiming, setClaiming] = useState(false); - const [claimError, setClaimError] = useState(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 ( -
- {/* Logo */} -
- -

SMIRK WALLET

-
- Smirk -
- -
- - {/* Loading state */} - {loading && ( -
-
-

Loading tip...

-
- )} - - {/* Error state */} - {error && !loading && ( -
-
- ! -
-
-

Tip Not Found

-

- {error} -

-
- - Go Home - -
- )} - - {/* Tip found */} - {tip && !loading && ( -
- {/* Status indicator */} - {tip.status === 'pending' ? ( -
- 🎁 -
- ) : tip.status === 'claimed' ? ( -
- - - -
- ) : ( -
- X -
- )} - - {/* Tip info */} -
- {coin && ( -
- {coin.name} - {formatAmount(tip.amount, tip.asset)} -
- )} - -
-

Status: {tip.status}

-

Created: {new Date(tip.created_at).toLocaleDateString()}

-
-
- - {/* Claim success */} - {claimSuccess && ( -
-

Claimed!

-

- Funds are being sent to your wallet. -

-

- TX: {claimSuccess.txid} -

-
- )} - - {/* Needs confirmations */} - {needsConfirmations && !claimSuccess && ( -
-

Waiting for Confirmations

-
-

This tip is still confirming on the blockchain.

-

- {tip?.funding_confirmations ?? 0} - {' / '} - {tip?.confirmations_required ?? 0} - {' confirmations'} -

-

- Refresh this page to check progress. -

-
-
- )} - - {/* Claim button for public tips */} - {isClaimable && !claimSuccess && ( -
-

- {hasExtension ? 'Ready to Claim!' : 'Install Smirk Wallet'} -

- - {hasExtension ? ( -
- - {claimError && ( -

{claimError}

- )} -
- ) : ( -
-

You need the Smirk Wallet extension to claim tips.

- - Install Smirk Wallet - -
- )} -
- )} - - {/* Targeted tip instructions (no fragment key) */} - {tip.status === 'pending' && !fragmentKey && !tip.is_public && ( -
-

- {hasExtension ? 'Claim This Tip' : 'Install Smirk Wallet'} -

- - {hasExtension ? ( -
-

To claim this tip:

-
    -
  1. Click the Smirk extension icon
  2. -
  3. Go to Inbox
  4. -
  5. Click Claim on this tip
  6. -
-
- ) : ( -
-

You need the Smirk Wallet extension to claim tips.

- - Install Smirk Wallet - -
- )} -
- )} - - {/* Public tip with missing fragment key */} - {tip.status === 'pending' && tip.is_public && !fragmentKey && ( -
-

Incomplete Link

-
-

This is a public tip, but the claim link is incomplete.

-

Make sure you have the full URL including the secret key after the # symbol.

-

- Example: smirk.cash/tip/abc123#secretKey -

-
-
- )} - - {tip.status === 'claimed' && !claimSuccess && ( -

This tip has already been claimed.

- )} - - {tip.status !== 'pending' && tip.status !== 'claimed' && ( -

This tip is no longer available.

- )} - - - Go Home - -
- )} -
- ); +export default async function TipPage({ params }: Props) { + const { id } = await params; + return ; } diff --git a/src/app/tips/page.tsx b/src/app/tips/page.tsx index fcbc999..b11a5a1 100644 --- a/src/app/tips/page.tsx +++ b/src/app/tips/page.tsx @@ -280,7 +280,7 @@ export default function TipsPage() {

Install the{' '}