// skinport.jsx — skintick.io Skinport API page

const { useState, useEffect, useRef } = React;

// Where the live /v1/status lives. Override at runtime by setting
// window.SKINTICK_API_BASE before this script loads.
const DEV_API_BASE = "http://127.0.0.1:8080";
const PROD_API_BASE = "https://api.skintick.io";
const API_BASE = (typeof window !== "undefined" && window.SKINTICK_API_BASE)
  || (["localhost", "127.0.0.1"].includes(window.location.hostname) ? DEV_API_BASE : PROD_API_BASE);

// MARKET_SLUG drives which row LiveStatus pulls out of /v1/status.
const MARKET_SLUG = "skinport";

const COVERAGE = [
  { label: "market type", value: "p2p" },
  { label: "catalog size", value: "24k+" },
  { label: "default pull", value: "5m" },
  { label: "currency", value: "USD" },
];

const FEATURES = [
  {
    title: "Batch latest prices",
    body: "Look up up to 100 market hash names per request and narrow the response to Skinport with the markets filter.",
  },
  {
    title: "Freshness metadata",
    body: "Every latest-price row includes collected_at, source_updated_at, staleness_seconds, and is_stale.",
  },
  {
    title: "History snapshots",
    body: "Fetch the stored Skinport price timeline for a single item when you need trend context instead of only the latest quote.",
  },
  {
    title: "Normalized responses",
    body: "Skinport uses the same public row shape as every other Skintick marketplace, which keeps client code simple.",
  },
  {
    title: "Confidence flags",
    body: "Cross-market analysis adds is_outlier and confidence so you can avoid acting on suspicious rows blindly.",
  },
  {
    title: "Spread-ready data",
    body: "Use the same normalized prices in /v1/spreads when you want Skinport compared against other p2p markets.",
  },
];

const ENDPOINTS = [
  { method: "POST", path: "/v1/prices/latest", blurb: "batch latest prices filtered to Skinport" },
  { method: "GET", path: "/v1/items/{market_hash_name}", blurb: "latest normalized rows for one item" },
  { method: "GET", path: "/v1/items/{market_hash_name}/history?market=skinport", blurb: "Skinport history snapshots for one item" },
  { method: "GET", path: "/v1/spreads?buy_market=skinport", blurb: "best routes that buy on Skinport" },
];

const RESPONSE_FIELDS = [
  {
    title: "market_hash_name",
    body: "The CS2 item identifier you asked for.",
  },
  {
    title: "market + market_type",
    body: "skinport plus its normalized marketplace category, currently p2p.",
  },
  {
    title: "price + currency",
    body: "The latest normalized price as a decimal string, with USD currency.",
  },
  {
    title: "listings_count",
    body: "How much visible depth supported the quote at collection time.",
  },
  {
    title: "freshness fields",
    body: "collected_at, source_updated_at, staleness_seconds, and is_stale.",
  },
  {
    title: "quality fields",
    body: "is_outlier and confidence for downstream filtering.",
  },
];

const USE_CASES = [
  {
    tag: "bots",
    title: "Price lookup loops",
    body: "Pull the latest Skinport price for many items in one request without building a custom client around one marketplace.",
  },
  {
    tag: "dashboards",
    title: "Freshness-aware UI",
    body: "Show users when a quote was collected and whether it is stale instead of pretending every price is equally live.",
  },
  {
    tag: "analytics",
    title: "History views",
    body: "Load Skinport snapshots for charts, comparisons, or backtests while keeping the response contract stable.",
  },
  {
    tag: "arbitrage",
    title: "Cross-market routing",
    body: "Use Skinport rows together with spread data, confidence, and outlier flags before deciding on a route.",
  },
];

const FAQS = [
  {
    q: "Is this an official Skinport API?",
    a: "No. Skintick is an independent service that exposes normalized Skinport pricing data through the Skintick API.",
  },
  {
    q: "What is the main Skinport API endpoint?",
    a: "For most apps it is POST /v1/prices/latest with markets set to [\"skinport\"]. That gives you batch lookups, missing items, warnings, and freshness metadata in one call.",
  },
  {
    q: "Can I get Skinport price history?",
    a: "Yes. Use GET /v1/items/{market_hash_name}/history?market=skinport to fetch stored snapshots for one item.",
  },
  {
    q: "How do I know if a Skinport price is stale?",
    a: "Use collected_at, staleness_seconds, and is_stale in the response. Skintick marks a latest-price row stale once it is older than the configured stale threshold.",
  },
  {
    q: "Why not integrate only Skinport directly?",
    a: "If you only ever need one market, you may not need an aggregator. Skintick becomes useful when you want the same contract across markets, freshness fields, history, and cross-market quality signals.",
  },
];

const SISTER_MARKETS = [
  { id: "buff", name: "BUFF163", href: "../buff.html", detail: "market page" },
  { id: "status", name: "Status", href: "../status/status.html", detail: "collector health" },
  { id: "home", name: "All markets", href: "../index.html#markets", detail: "overview" },
];

const SAMPLE_JSON = `{
  "data": [
    {
      "market_hash_name": "AK-47 | Redline (Field-Tested)",
      "prices": [
        {
          "market": "skinport",
          "market_type": "p2p",
          "price": "32.79",
          "currency": "USD",
          "listings_count": 546,
          "collected_at": "2026-05-15T16:17:05Z",
          "source_updated_at": null,
          "staleness_seconds": 142,
          "is_stale": false,
          "is_outlier": false,
          "confidence": "high"
        }
      ]
    }
  ],
  "missing": [],
  "warnings": []
}`;

// formatAgo turns a "ms ago" number into "5s" / "12m" / "3h" / "2d".
// Tabular-nums in CSS keeps the width steady as the value ticks up.
function formatAgo(ms) {
  if (ms === null || ms === undefined || ms < 0) return "?";
  const s = Math.floor(ms / 1000);
  if (s < 60) return `${s}s`;
  const m = Math.floor(s / 60);
  if (m < 60) return `${m}m`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h`;
  return `${Math.floor(h / 24)}d`;
}

// LIVE_LABEL is what we put in the leftmost "tag" slot per status state.
// Healthy = LIVE, the same word every status page uses, so it's familiar.
const LIVE_LABEL = {
  healthy: "LIVE",
  degraded: "DEGRADED",
  unavailable: "DOWN",
  unknown: "NO DATA",
  loading: "—",
};

// LiveStatus polls /v1/status every 30s, ticks the "ago" counter every
// second locally so the timestamp feels alive without spamming the API.
// Failure to fetch leaves the badge in an "unavailable" state — that's
// honest signal when the API itself is the thing that's down.
function LiveStatus({ slug = MARKET_SLUG, apiBase = API_BASE }) {
  const [row, setRow] = useState(null);
  const [status, setStatus] = useState("loading");
  const [now, setNow] = useState(() => Date.now());

  useEffect(() => {
    let cancelled = false;
    async function load() {
      try {
        const res = await fetch(`${apiBase}/v1/status`, { cache: "no-store" });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        if (cancelled) return;
        const match = (json.markets || []).find((m) => m.market === slug);
        if (!match) {
          setRow(null);
          setStatus("unknown");
          return;
        }
        setRow(match);
        setStatus(match.status || "unknown");
      } catch (err) {
        if (cancelled) return;
        setRow(null);
        setStatus("unavailable");
      }
    }
    load();
    const t = setInterval(load, 30000);
    return () => { cancelled = true; clearInterval(t); };
  }, [slug, apiBase]);

  useEffect(() => {
    const t = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(t);
  }, []);

  const lastSuccessMs = row && row.last_success_at ? Date.parse(row.last_success_at) : null;
  const ago = lastSuccessMs ? formatAgo(now - lastSuccessMs) : null;
  const pullSec = row && row.last_run_duration_ms != null
    ? (row.last_run_duration_ms / 1000).toFixed(1)
    : null;

  return (
    <div className={`live-status live-banner live-status-${status}`} role="status" aria-live="polite">
      <span className="live-tag">
        <span className="live-dot" aria-hidden="true" />
        <span className="live-label">{LIVE_LABEL[status] || "—"}</span>
      </span>
      <span className="live-sep">·</span>
      <span className="live-detail"><b>{slug}</b></span>
      {ago && (
        <>
          <span className="live-sep">·</span>
          <span className="live-detail">collected <b>{ago}</b> ago</span>
        </>
      )}
      {pullSec && (
        <>
          <span className="live-sep">·</span>
          <span className="live-detail"><b>{pullSec}s</b> pull</span>
        </>
      )}
    </div>
  );
}

function PixelLandscape() {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || !window.LandscapeAPI) return;
    window.LandscapeAPI.mount(ref.current);
  }, []);
  return <canvas ref={ref} className="landscape-canvas" />;
}

function Nav() {
  return (
    <nav className="nav">
      <a href="../index.html" className="brand">
        <img src="/logos/favicon.svg" alt="" className="brand-mark" />
        <span>skintick.io</span>
      </a>
      <div className="nav-links">
        <a href="../index.html#about">about</a>
        <a href="../index.html#markets">markets</a>
        <a href="../index.html#api">api</a>
        <a href="../index.html#pricing">pricing</a>
        <a href="#" className="text-link">log in</a>
        <a href="#" className="btn btn-primary">sign up</a>
      </div>
    </nav>
  );
}

function Hero() {
  return (
    <header className="hero hero-market" data-screen-label="00 Skinport Hero">
      <PixelLandscape />
      <div className="hero-content">
        <div className="market-crumb">
          <a href="../index.html#markets">markets</a>
          <span className="crumb-sep">/</span>
          <span>Skinport API</span>
        </div>
        <h1 className="hero-title">
          Skinport API for<br/>
          CS2 prices.
        </h1>
        <p className="hero-sub">
          Query Skinport through one normalized Skintick contract: batch latest prices, history snapshots, freshness metadata, and cross-market quality flags for Counter-Strike 2 items.
        </p>
        <div className="hero-cta">
          <a href="#sample" className="btn btn-primary">see example response</a>
          <a href="#endpoints" className="btn btn-secondary">view endpoints</a>
          <div className="hero-foot">
            <span>p2p</span>
            <span className="hero-foot-sep">·</span>
            <span><b>24k+</b> items</span>
            <span className="hero-foot-sep">·</span>
            <span><b>5-min</b> updates</span>
            <span className="hero-foot-sep">·</span>
            <span>USD</span>
          </div>
          <LiveStatus />
        </div>
      </div>
      <div className="hero-fade" aria-hidden="true" />
    </header>
  );
}

function Coverage() {
  return (
    <section className="coverage">
      {COVERAGE.map((item) => (
        <div key={item.label} className="stat">
          <div className="stat-value">{item.value}</div>
          <div className="stat-label">{item.label}</div>
        </div>
      ))}
    </section>
  );
}

function FrameBox({ label, children, id, wide }) {
  return (
    <section className={"framebox " + (wide ? "framebox-wide" : "")} id={id}>
      <div className="framebox-label">{label}</div>
      <div className="framebox-body">{children}</div>
    </section>
  );
}

function Overview() {
  return (
    <FrameBox label="overview" id="overview">
      <p>
        <b>Skintick's Skinport API</b> gives developers a clean way to use Skinport pricing data without building a one-off integration path for one marketplace.
      </p>
      <p>
        The useful bit is not only the latest price. It is the surrounding context: when the row was collected, whether it is stale, how much visible depth supported it, and whether cross-market checks consider it suspicious.
      </p>
    </FrameBox>
  );
}

function WhySkinport() {
  return (
    <FrameBox label="why use it" id="why">
      <ul className="bullets">
        <li><b>Batch-friendly.</b> One latest-price call can return many Skinport items at once.</li>
        <li><b>Operationally honest.</b> Freshness is explicit instead of hidden behind a single number.</li>
        <li><b>Multi-market by design.</b> The same client code can consume Skinport, CSFloat, White.market, and the rest of the Skintick set.</li>
        <li><b>Better downstream decisions.</b> Outlier and confidence flags let bots filter dubious rows before doing math.</li>
      </ul>
    </FrameBox>
  );
}

function Features() {
  return (
    <FrameBox label="what's in the feed" id="features" wide>
      <div className="feat-grid">
        {FEATURES.map((feature) => (
          <div key={feature.title} className="feat">
            <div className="feat-title">{feature.title}</div>
            <div className="feat-body">{feature.body}</div>
          </div>
        ))}
      </div>
    </FrameBox>
  );
}

function Endpoints() {
  return (
    <FrameBox label="endpoints" id="endpoints" wide>
      <div className="endpoint-table">
        <div className="endpoint-row endpoint-head">
          <span>method</span><span>path</span><span>description</span>
        </div>
        {ENDPOINTS.map((endpoint) => (
          <div key={endpoint.path} className="endpoint-row">
            <span className={"endpoint-method " + (endpoint.method === "POST" ? "method-post" : "method-get")}>{endpoint.method}</span>
            <code className="endpoint-path">{endpoint.path}</code>
            <span className="endpoint-blurb">{endpoint.blurb}</span>
          </div>
        ))}
      </div>
    </FrameBox>
  );
}

function CodeBlock() {
  const ref = useRef(null);
  const [typed, setTyped] = useState("");
  const [done, setDone] = useState(false);

  useEffect(() => {
    if (!ref.current) return;
    let i = 0;
    let raf;
    const total = SAMPLE_JSON.length;
    const tick = () => {
      i = Math.min(total, i + 22);
      setTyped(SAMPLE_JSON.slice(0, i));
      if (i < total) raf = requestAnimationFrame(tick);
      else setDone(true);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  return (
    <FrameBox label="sample response" id="sample" wide>
      <div className="api-grid">
        <div className="api-left">
          <p className="kicker">POST</p>
          <code className="endpoint">/v1/prices/latest</code>
          <p className="api-desc">
            Ask for several items, pin the request to Skinport, and receive explicit missing items plus quality metadata on each returned row.
          </p>
          <div className="curl">
            <span className="curl-prompt">$</span> curl -X POST -H "Authorization: Bearer skt_..." \<br/>
            <span className="curl-cont">  </span>-H "Content-Type: application/json" \<br/>
            <span className="curl-cont">  </span>-d '{`{"items":["AK-47 | Redline (Field-Tested)"],"markets":["skinport"]}`}' \<br/>
            <span className="curl-cont">  </span>https://api.skintick.io/v1/prices/latest
          </div>
        </div>
        <pre className="code-pane" ref={ref}>
          <div className="code-pane-bar">
            <span className="code-dot" style={{ background: "#ff5f56" }} />
            <span className="code-dot" style={{ background: "#ffbd2e" }} />
            <span className="code-dot" style={{ background: "#27c93f" }} />
            <span className="code-pane-title">skinport.latest.response.json</span>
          </div>
          <code className="code-pane-body">
            {typed}
            {!done && <span className="caret">█</span>}
          </code>
        </pre>
      </div>
    </FrameBox>
  );
}

function ResponseFields() {
  return (
    <FrameBox label="response fields" id="fields" wide>
      <div className="feat-grid">
        {RESPONSE_FIELDS.map((field) => (
          <div key={field.title} className="feat">
            <div className="feat-title">{field.title}</div>
            <div className="feat-body">{field.body}</div>
          </div>
        ))}
      </div>
    </FrameBox>
  );
}

function UseCases() {
  return (
    <FrameBox label="what people build" id="use-cases" wide>
      <div className="usecase-grid">
        {USE_CASES.map((useCase) => (
          <div key={useCase.title} className="usecase">
            <div className="usecase-tag">{useCase.tag}</div>
            <div className="usecase-title">{useCase.title}</div>
            <div className="usecase-body">{useCase.body}</div>
          </div>
        ))}
      </div>
    </FrameBox>
  );
}

function FAQ() {
  const [open, setOpen] = useState(0);
  return (
    <FrameBox label="faq" id="faq" wide>
      <div className="faq-list">
        {FAQS.map((item, index) => (
          <div key={item.q} className={"faq-item " + (open === index ? "open" : "")}>
            <button className="faq-q" onClick={() => setOpen(open === index ? -1 : index)}>
              <span className="faq-marker">{open === index ? "[-]" : "[+]"}</span>
              {item.q}
            </button>
            {open === index && <div className="faq-a">{item.a}</div>}
          </div>
        ))}
      </div>
    </FrameBox>
  );
}

function RelatedPages() {
  return (
    <FrameBox label="related pages" id="related" wide>
      <div className="sisters">
        {SISTER_MARKETS.map((item) => (
          <a key={item.id} href={item.href} className="sister">
            <span className="sister-name">{item.name}</span>
            <span className="sister-p50">{item.detail}</span>
            <span className="sister-arrow">-&gt;</span>
          </a>
        ))}
      </div>
    </FrameBox>
  );
}

function StartCTA() {
  return (
    <section className="start-cta" id="start" data-screen-label="99 CTA">
      <div className="start-inner">
        <h2 className="start-title">Build against Skinport once.</h2>
        <p className="start-sub">
          Keep the same response contract when you add more marketplaces later: latest prices, history, freshness, and quality flags in one API.
        </p>
        <div className="start-form">
          <a href="../index.html#pricing" className="btn btn-primary start-btn">view plans</a>
          <a href="#sample" className="btn btn-secondary">inspect payload</a>
        </div>
      </div>
    </section>
  );
}

function Footer() {
  return (
    <footer className="footer">
      <div className="footer-grid">
        <div className="footer-col">
          <div className="footer-brand">
            <img src="/logos/favicon.svg" alt="" className="footer-brand-mark" />
            <span>skintick.io</span>
          </div>
          <div className="footer-tag">price intelligence for the CS2 skin economy.</div>
        </div>
        <div className="footer-col">
          <div className="footer-h">product</div>
          <a href="../index.html#api">API reference</a>
          <a href="../status/status.html">Status</a>
          <a href="../index.html#pricing">Pricing</a>
        </div>
        <div className="footer-col">
          <div className="footer-h">markets</div>
          <a href="./">Skinport API</a>
          <a href="../buff.html">BUFF163</a>
          <a href="../index.html#markets">All markets</a>
        </div>
        <div className="footer-col">
          <div className="footer-h">company</div>
          <a href="../index.html#about">About</a>
          <a href="../terms/">Terms</a>
          <a href="../privacy/">Privacy</a>
          <a href="../refund/">Refund Policy</a>
          <a href="mailto:support@skintick.io">Contact</a>
        </div>
      </div>
      <p className="footer-disclaimer">
        Skintick is not affiliated with Valve Corporation, Steam, Counter-Strike, Skinport, CSFloat, Tradeit, DMarket, or any other marketplace. All trademarks belong to their respective owners. Data is provided as-is and may be delayed or inaccurate.
      </p>
      <div className="footer-bottom">
        <span>© 2026 skintick.io</span>
        <span>data is provided as-is.</span>
      </div>
    </footer>
  );
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "midnight",
  "density": 4
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  useEffect(() => {
    if (window.LandscapeAPI) window.LandscapeAPI.setTheme(t.theme);
  }, [t.theme]);
  useEffect(() => {
    if (window.LandscapeAPI) window.LandscapeAPI.setDensity(t.density);
  }, [t.density]);

  return (
    <div className="page">
      <Nav />
      <Hero />
      <main className="main">
        <Overview />
        <WhySkinport />
        <Coverage />
        <Features />
        <Endpoints />
        <CodeBlock />
        <ResponseFields />
        <UseCases />
        <FAQ />
        <RelatedPages />
      </main>
      <StartCTA />
      <Footer />

      <TweaksPanel>
        <TweakSection label="Scene" />
        <TweakRadio
          label="Theme"
          value={t.theme}
          options={[
            { value: "midnight", label: "midnight" },
            { value: "sunset", label: "sunset" },
            { value: "matrix", label: "matrix" },
            { value: "dusk", label: "dusk" },
          ]}
          onChange={(value) => setTweak("theme", value)}
        />
        <TweakSlider
          label="Pixel size"
          value={t.density}
          min={2}
          max={8}
          step={1}
          onChange={(value) => setTweak("density", value)}
        />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
