// frado.near/widget/CosmoPOS.Dashboard_fraDAO_Cosmica // POS Display — Bar / Restaurante / Loja — Fazenda Cósmica // Design: FRADO — v1.0 const apiKey = props.apiKey || "4ec1be5a-4c74-4f4a-ad1c-a16718db4af7"; const defToken = props.token || "cos.tkn.near"; const defTarget = props.target || "fazendacosmica.near"; State.init({ token: defToken, target: defTarget, tokenTemp: "", targetTemp: "", loading: true, data: null, tokenMetadata: null, metaFetched: false, intervalStarted: false, editToken: false, editTarget: false, }); // ─── Fetch ──────────────────────────────────────────────────────────────────── const fetchTx = () => { const res = fetch( `https://api.pikespeak.ai/account/ft-transfer/${state.target}`, { headers: { "x-api-key": apiKey } } ); if (!res || !res.ok) return; const data = res.body .filter((i) => i.contract === state.token && i.amount !== "0") .map((i) => { const d = new Date(i.timestamp / 1e6); return { tx: i.transaction_id, status: i.status, sender: i.sender, recv: i.receiver, amount: parseInt(i.amount).toLocaleString("pt-BR"), date: d.toLocaleDateString("pt-BR"), time: d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", second: "2-digit", }), ts: i.timestamp, }; }) .sort((a, b) => b.ts - a.ts); State.update({ data, loading: false }); }; const fetchMeta = () => { Near.asyncView(state.token, "ft_metadata", {}, "final", false).then((m) => State.update({ tokenMetadata: m, metaFetched: true }) ); }; fetchTx(); fetchMeta(); if (!state.intervalStarted) { State.update({ intervalStarted: true }); setInterval(fetchTx, 3000); } // ─── Loading ────────────────────────────────────────────────────────────────── if (state.loading || !state.metaFetched) { return ( <div style={{ background: "#0A0A0A", minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'Syne', sans-serif", color: "#D4AF37", fontSize: "1rem", letterSpacing: "0.25em", }} > CARREGANDO... </div> ); } // ─── Styles ─────────────────────────────────────────────────────────────────── const Wrap = styled.div` background: #0A0A0A; min-height: 100vh; font-family: 'Inter', sans-serif; color: #EDEDED; `; const Header = styled.div` background: #0D0D0D; border-bottom: 1px solid rgba(255,255,255,0.06); padding: 12px 20px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; `; const Logo = styled.span` font-family: 'Syne', sans-serif; font-weight: 900; font-size: 1rem; letter-spacing: 0.2em; color: #D4AF37; text-transform: uppercase; flex-shrink: 0; `; const Chip = styled.div` display: flex; align-items: center; gap: 6px; background: #161616; border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 6px 12px; cursor: pointer; transition: border-color 0.2s; &:hover { border-color: rgba(212,175,55,0.5); } `; const ChipLabel = styled.span` font-family: 'Syne', sans-serif; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255,255,255,0.35); `; const ChipVal = styled.span` font-size: 0.82rem; color: #EDEDED; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; const InlineInput = styled.input` background: transparent; border: none; border-bottom: 1px solid rgba(212,175,55,0.5); color: #EDEDED; font-size: 0.82rem; padding: 2px 4px; width: 180px; outline: none; `; const LiveBadge = styled.div` margin-left: auto; display: flex; align-items: center; gap: 7px; font-family: 'Syne', sans-serif; font-size: 0.65rem; letter-spacing: 0.12em; color: rgba(255,255,255,0.28); `; const PulseDot = styled.span` width: 8px; height: 8px; border-radius: 50%; background: #4ADE80; box-shadow: 0 0 8px rgba(74,222,128,0.7); display: inline-block; animation: blink 2s ease-in-out infinite; @keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } } `; const Body = styled.div` padding: 20px; max-width: 1000px; margin: 0 auto; `; // Hero card const Hero = styled.div` border-radius: 10px; padding: 28px 28px 24px; margin-bottom: 24px; border: 1px solid ${(p) => p.ok ? "rgba(74,222,128,0.22)" : "rgba(248,113,113,0.22)"}; background: ${(p) => p.ok ? "linear-gradient(140deg, rgba(74,222,128,0.07) 0%, #0A0A0A 55%)" : "linear-gradient(140deg, rgba(248,113,113,0.09) 0%, #0A0A0A 55%)"}; `; const HeroEyebrow = styled.div` font-family: 'Syne', sans-serif; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; color: rgba(255,255,255,0.3); margin-bottom: 14px; `; const StatusPill = styled.div` display: inline-flex; align-items: center; gap: 8px; padding: 7px 18px; border-radius: 99px; font-family: 'Syne', sans-serif; font-size: 0.88rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; background: ${(p) => p.ok ? "rgba(74,222,128,0.15)" : "rgba(248,113,113,0.15)"}; color: ${(p) => (p.ok ? "#4ADE80" : "#F87171")}; border: 1px solid ${(p) => p.ok ? "rgba(74,222,128,0.35)" : "rgba(248,113,113,0.35)"}; margin-bottom: 18px; `; const HeroSender = styled.div` font-family: 'Syne', sans-serif; font-size: 1.6rem; font-weight: 700; color: #FFFFFF; margin-bottom: 6px; word-break: break-all; line-height: 1.2; `; const HeroAmount = styled.div` font-family: 'Syne', sans-serif; font-size: 2.6rem; font-weight: 900; color: #D4AF37; letter-spacing: 0.01em; margin-bottom: 18px; line-height: 1; `; const HeroFooter = styled.div` display: flex; align-items: center; gap: 18px; flex-wrap: wrap; `; const HeroTime = styled.span` font-size: 0.95rem; color: rgba(255,255,255,0.4); font-variant-numeric: tabular-nums; `; const TxBtn = styled.a` display: inline-flex; align-items: center; gap: 5px; font-family: 'Syne', sans-serif; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: #D4AF37; text-decoration: none; border: 1px solid rgba(212,175,55,0.3); border-radius: 4px; padding: 6px 12px; transition: all 0.2s; &:hover { background: rgba(212,175,55,0.1); border-color: rgba(212,175,55,0.6); color: #D4AF37; text-decoration: none; } `; // Histórico const SecLabel = styled.div` font-family: 'Syne', sans-serif; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: rgba(255,255,255,0.25); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); `; const ColHeads = styled.div` display: grid; grid-template-columns: 22px 1fr 110px 110px 88px 80px 32px; gap: 10px; padding: 4px 12px 6px; font-family: 'Syne', sans-serif; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(255,255,255,0.22); @media (max-width: 640px) { grid-template-columns: 22px 1fr 90px 80px 32px; } `; const Row = styled.div` display: grid; grid-template-columns: 22px 1fr 110px 110px 88px 80px 32px; gap: 10px; align-items: center; padding: 11px 12px; background: #111111; border: 1px solid rgba(255,255,255,0.04); border-radius: 6px; margin-bottom: 2px; transition: background 0.15s; &:hover { background: #161616; } @media (max-width: 640px) { grid-template-columns: 22px 1fr 90px 80px 32px; } `; const StatusDot = styled.span` display: block; width: 10px; height: 10px; border-radius: 50%; background: ${(p) => (p.ok ? "#4ADE80" : "#F87171")}; box-shadow: ${(p) => p.ok ? "0 0 7px rgba(74,222,128,0.65)" : "0 0 7px rgba(248,113,113,0.65)"}; margin: 0 auto; flex-shrink: 0; `; const C = styled.div` font-size: 0.84rem; color: ${(p) => (p.muted ? "rgba(255,255,255,0.38)" : "#EDEDED")}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; const Amt = styled.div` font-family: 'Syne', sans-serif; font-weight: 700; font-size: 0.88rem; color: #D4AF37; text-align: right; `; // ─── Data ───────────────────────────────────────────────────────────────────── const rows = state.data || []; const latest = rows[0]; const hist = rows.slice(1); const sym = state.tokenMetadata?.symbol || "TOKEN"; // ─── Render ─────────────────────────────────────────────────────────────────── return ( <Wrap> <Header> <Logo> POS</Logo> {/* Seletor de Caixa (target) */} <Chip onClick={() => State.update({ editTarget: !state.editTarget, editToken: false }) } > <ChipLabel>Caixa</ChipLabel> {state.editTarget ? ( <> <InlineInput autoFocus defaultValue={state.target} placeholder="conta.near" onChange={(e) => State.update({ targetTemp: e.target.value })} onKeyDown={(e) => { if (e.key === "Enter" && state.targetTemp) { State.update({ target: state.targetTemp, editTarget: false, loading: true, }); fetchTx(); } if (e.key === "Escape") State.update({ editTarget: false }); }} /> <span style={{ fontSize: "0.7rem", color: "rgba(212,175,55,0.6)" }}> ↵ </span> </> ) : ( <ChipVal>{state.target}</ChipVal> )} </Chip> {/* Seletor de Token */} <Chip onClick={() => State.update({ editToken: !state.editToken, editTarget: false }) } > {state.tokenMetadata?.icon && ( <img src={state.tokenMetadata.icon} style={{ width: 15, height: 15, borderRadius: "50%" }} /> )} <ChipLabel>Token</ChipLabel> {state.editToken ? ( <> <InlineInput autoFocus defaultValue={state.token} placeholder="token.tkn.near" onChange={(e) => State.update({ tokenTemp: e.target.value })} onKeyDown={(e) => { if (e.key === "Enter" && state.tokenTemp) { State.update({ token: state.tokenTemp, editToken: false, loading: true, metaFetched: false, }); fetchTx(); fetchMeta(); } if (e.key === "Escape") State.update({ editToken: false }); }} /> <span style={{ fontSize: "0.7rem", color: "rgba(212,175,55,0.6)" }}> ↵ </span> </> ) : ( <ChipVal>{sym}</ChipVal> )} </Chip> <LiveBadge> <PulseDot /> LIVE · 10s </LiveBadge> </Header> <Body> {/* ── Último pagamento ── */} {latest ? ( <Hero ok={latest.status}> <HeroEyebrow>Último pagamento</HeroEyebrow> <StatusPill ok={latest.status}> {latest.status ? "✓ Aprovado" : "✕ Falhou"} </StatusPill> <HeroSender>{latest.sender}</HeroSender> <HeroAmount> {latest.amount} {sym} </HeroAmount> <HeroFooter> <HeroTime> {latest.date} · {latest.time} </HeroTime> <TxBtn href={`https://nearblocks.io/txns/${latest.tx}`} target="_blank" rel="noopener noreferrer" > ↗ Ver transação </TxBtn> </HeroFooter> </Hero> ) : ( <Hero ok={true} style={{ textAlign: "center", padding: "48px 28px" }}> <HeroEyebrow style={{ textAlign: "center" }}> Aguardando pagamentos </HeroEyebrow> <div style={{ color: "rgba(255,255,255,0.2)", fontFamily: "'Syne',sans-serif", fontSize: "1.1rem", }} > Nenhuma transação encontrada </div> </Hero> )} {/* ── Histórico ── */} {hist.length > 0 && ( <> <SecLabel>Histórico</SecLabel> <ColHeads> <div></div> <div>Remetente</div> <div style={{ textAlign: "right" }}>Valor</div> <div className="hide-mobile">Destinatário</div> <div className="hide-mobile">Data</div> <div>Hora</div> <div></div> </ColHeads> {hist.map((r, i) => ( <Row key={i}> <div style={{ display: "flex", alignItems: "center", justifyContent: "center", }} > <StatusDot ok={r.status} /> </div> <C>{r.sender}</C> <Amt> {r.amount} {sym} </Amt> <C muted className="hide-mobile"> {r.recv} </C> <C muted className="hide-mobile"> {r.date} </C> <C muted>{r.time}</C> <div> <a href={`https://nearblocks.io/txns/${r.tx}`} target="_blank" rel="noopener noreferrer" style={{ color: "rgba(212,175,55,0.55)", fontSize: "1rem", textDecoration: "none", }} title="Ver transação" > ↗ </a> </div> </Row> ))} </> )} </Body> </Wrap> );