repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Unified chat aggregator — Twitch + X + Kick in one…
stars
latest
clone command
git clone gitlawb://did:key:z6MkfpiH...NPsu/unified-chat-ag...git clone gitlawb://did:key:z6MkfpiH.../unified-chat-ag...5a763564sync from playground1d ago| #1 | import { useState, useCallback } from "react"; |
| #2 | import { |
| #3 | useAccount, |
| #4 | useConnect, |
| #5 | useDisconnect, |
| #6 | useWriteContract, |
| #7 | useWaitForTransactionReceipt, |
| #8 | } from "wagmi"; |
| #9 | import { parseUnits } from "viem"; |
| #10 | import { RECIPIENT, USDC_BASE, ERC20_ABI } from "./wagmi.ts"; |
| #11 | |
| #12 | const PRESETS = [1, 5, 20] as const; |
| #13 | |
| #14 | type Phase = "pick" | "confirm" | "sending" | "done" | "no-wallet"; |
| #15 | |
| #16 | export default function TipPanel({ onClose }: { onClose: () => void }) { |
| #17 | const { address, isConnected } = useAccount(); |
| #18 | const { connect, connectors, isPending: connecting } = useConnect(); |
| #19 | const { disconnect } = useDisconnect(); |
| #20 | |
| #21 | const [amount, setAmount] = useState<string>("5"); |
| #22 | const [customMode, setCustomMode] = useState(false); |
| #23 | const [phase, setPhase] = useState<Phase>(isConnected ? "pick" : "pick"); |
| #24 | const [txHash, setTxHash] = useState<`0x${string}` | undefined>(); |
| #25 | |
| #26 | const { writeContract, isPending: writing } = useWriteContract(); |
| #27 | |
| #28 | const { isSuccess: confirmed } = |
| #29 | useWaitForTransactionReceipt({ hash: txHash }); |
| #30 | |
| #31 | // Detect if any wallet is available |
| #32 | const hasInjected = typeof window !== "undefined" && !!window.ethereum; |
| #33 | |
| #34 | const handleConnect = useCallback(() => { |
| #35 | if (!hasInjected) { |
| #36 | setPhase("no-wallet"); |
| #37 | return; |
| #38 | } |
| #39 | const injected = connectors.find((c) => c.id === "injected"); |
| #40 | if (injected) { |
| #41 | connect({ connector: injected }); |
| #42 | } |
| #43 | }, [connect, connectors, hasInjected]); |
| #44 | |
| #45 | const handleSend = useCallback(() => { |
| #46 | const value = parseUnits(amount, 6); // USDC has 6 decimals |
| #47 | writeContract( |
| #48 | { |
| #49 | address: USDC_BASE, |
| #50 | abi: ERC20_ABI, |
| #51 | functionName: "transfer", |
| #52 | args: [RECIPIENT, value], |
| #53 | }, |
| #54 | { |
| #55 | onSuccess: (hash) => { |
| #56 | setTxHash(hash); |
| #57 | setPhase("sending"); |
| #58 | }, |
| #59 | onError: () => { |
| #60 | setPhase("pick"); // reset on error |
| #61 | }, |
| #62 | }, |
| #63 | ); |
| #64 | }, [amount, writeContract]); |
| #65 | |
| #66 | // Transition to done when confirmed |
| #67 | if (confirmed && phase === "sending") { |
| #68 | setPhase("done"); |
| #69 | } |
| #70 | |
| #71 | const explorerUrl = txHash |
| #72 | ? `https://basescan.org/tx/${txHash}` |
| #73 | : undefined; |
| #74 | |
| #75 | return ( |
| #76 | <div className="tip-overlay" onClick={onClose}> |
| #77 | <div className="tip-modal" onClick={(e) => e.stopPropagation()}> |
| #78 | {/* Close */} |
| #79 | <button className="tip-close" onClick={onClose}> |
| #80 | <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12"/></svg> |
| #81 | </button> |
| #82 | |
| #83 | {/* No wallet state */} |
| #84 | {phase === "no-wallet" && ( |
| #85 | <div className="tip-phase"> |
| #86 | <div className="tip-icon-circle tip-icon-warn"> |
| #87 | <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> |
| #88 | </div> |
| #89 | <h3 className="tip-title">No Wallet Detected</h3> |
| #90 | <p className="tip-desc">Install a browser wallet like MetaMask or Coinbase Wallet to send crypto tips on Base.</p> |
| #91 | <a |
| #92 | className="tip-btn tip-btn-primary" |
| #93 | href="https://metamask.io/download/" |
| #94 | target="_blank" |
| #95 | rel="noopener noreferrer" |
| #96 | > |
| #97 | Install MetaMask |
| #98 | </a> |
| #99 | <button className="tip-btn tip-btn-ghost" onClick={() => setPhase("pick")}> |
| #100 | Go back |
| #101 | </button> |
| #102 | </div> |
| #103 | )} |
| #104 | |
| #105 | {/* Pick amount */} |
| #106 | {phase === "pick" && ( |
| #107 | <div className="tip-phase"> |
| #108 | <div className="tip-icon-circle"> |
| #109 | <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"> |
| #110 | <circle cx="12" cy="12" r="10"/> |
| #111 | <text x="12" y="16" textAnchor="middle" fill="currentColor" stroke="none" fontSize="12" fontWeight="700">$</text> |
| #112 | </svg> |
| #113 | </div> |
| #114 | <h3 className="tip-title">Support this project</h3> |
| #115 | <p className="tip-desc">Send USDC on Base to power the real chat.</p> |
| #116 | |
| #117 | {isConnected && ( |
| #118 | <div className="tip-wallet-chip"> |
| #119 | <span className="tip-wallet-dot" /> |
| #120 | {address?.slice(0, 6)}…{address?.slice(-4)} |
| #121 | <button className="tip-disconnect" onClick={() => disconnect()}>Disconnect</button> |
| #122 | </div> |
| #123 | )} |
| #124 | |
| #125 | {/* Amount presets */} |
| #126 | <div className="tip-presets"> |
| #127 | {PRESETS.map((v) => ( |
| #128 | <button |
| #129 | key={v} |
| #130 | className={`tip-preset ${!customMode && amount === String(v) ? "active" : ""}`} |
| #131 | onClick={() => { setAmount(String(v)); setCustomMode(false); }} |
| #132 | > |
| #133 | ${v} |
| #134 | </button> |
| #135 | ))} |
| #136 | <button |
| #137 | className={`tip-preset ${customMode ? "active" : ""}`} |
| #138 | onClick={() => setCustomMode(true)} |
| #139 | > |
| #140 | Custom |
| #141 | </button> |
| #142 | </div> |
| #143 | |
| #144 | {customMode && ( |
| #145 | <div className="tip-custom-row"> |
| #146 | <span className="tip-currency-sign">$</span> |
| #147 | <input |
| #148 | className="tip-custom-input" |
| #149 | type="number" |
| #150 | min="0.01" |
| #151 | step="0.01" |
| #152 | placeholder="0.00" |
| #153 | value={amount} |
| #154 | onChange={(e) => setAmount(e.target.value)} |
| #155 | autoFocus |
| #156 | /> |
| #157 | <span className="tip-currency-label">USDC</span> |
| #158 | </div> |
| #159 | )} |
| #160 | |
| #161 | {!isConnected ? ( |
| #162 | <button className="tip-btn tip-btn-primary" onClick={handleConnect} disabled={connecting}> |
| #163 | {connecting ? "Connecting…" : "Connect Wallet"} |
| #164 | </button> |
| #165 | ) : ( |
| #166 | <button |
| #167 | className="tip-btn tip-btn-primary" |
| #168 | onClick={() => setPhase("confirm")} |
| #169 | disabled={!amount || Number(amount) <= 0} |
| #170 | > |
| #171 | Continue |
| #172 | </button> |
| #173 | )} |
| #174 | </div> |
| #175 | )} |
| #176 | |
| #177 | {/* Confirm */} |
| #178 | {phase === "confirm" && ( |
| #179 | <div className="tip-phase"> |
| #180 | <div className="tip-icon-circle tip-icon-confirm"> |
| #181 | <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="20 6 9 17 4 12"/></svg> |
| #182 | </div> |
| #183 | <h3 className="tip-title">Confirm payment</h3> |
| #184 | <div className="tip-confirm-details"> |
| #185 | <div className="tip-confirm-row"> |
| #186 | <span>Amount</span> |
| #187 | <strong>{amount} USDC</strong> |
| #188 | </div> |
| #189 | <div className="tip-confirm-row"> |
| #190 | <span>Network</span> |
| #191 | <strong>Base</strong> |
| #192 | </div> |
| #193 | <div className="tip-confirm-row"> |
| #194 | <span>To</span> |
| #195 | <strong className="tip-addr">{RECIPIENT.slice(0, 8)}…{RECIPIENT.slice(-6)}</strong> |
| #196 | </div> |
| #197 | </div> |
| #198 | <button className="tip-btn tip-btn-primary" onClick={handleSend} disabled={writing}> |
| #199 | {writing ? "Check wallet…" : `Send ${amount} USDC`} |
| #200 | </button> |
| #201 | <button className="tip-btn tip-btn-ghost" onClick={() => setPhase("pick")}>Back</button> |
| #202 | </div> |
| #203 | )} |
| #204 | |
| #205 | {/* Sending / Confirming */} |
| #206 | {phase === "sending" && ( |
| #207 | <div className="tip-phase"> |
| #208 | <div className="tip-spinner" /> |
| #209 | <h3 className="tip-title">Confirming transaction…</h3> |
| #210 | <p className="tip-desc">Waiting for Base network confirmation.</p> |
| #211 | {explorerUrl && ( |
| #212 | <a className="tip-link" href={explorerUrl} target="_blank" rel="noopener noreferrer"> |
| #213 | View on BaseScan ↗ |
| #214 | </a> |
| #215 | )} |
| #216 | </div> |
| #217 | )} |
| #218 | |
| #219 | {/* Done */} |
| #220 | {phase === "done" && ( |
| #221 | <div className="tip-phase"> |
| #222 | <div className="tip-icon-circle tip-icon-success"> |
| #223 | <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg> |
| #224 | </div> |
| #225 | <h3 className="tip-title">Thank you!</h3> |
| #226 | <p className="tip-desc">Your {amount} USDC tip has been sent. You're keeping the real chat alive.</p> |
| #227 | {explorerUrl && ( |
| #228 | <a className="tip-link" href={explorerUrl} target="_blank" rel="noopener noreferrer"> |
| #229 | View transaction on BaseScan ↗ |
| #230 | </a> |
| #231 | )} |
| #232 | <button className="tip-btn tip-btn-primary" onClick={onClose}>Done</button> |
| #233 | </div> |
| #234 | )} |
| #235 | </div> |
| #236 | </div> |
| #237 | ); |
| #238 | } |
| #239 |