// Portfolio — Daniel Kelleher

const { useState, useMemo, useEffect } = React;

const LABEL_FONTS = {
  "Geist Mono":      `"Geist Mono", ui-monospace, monospace`,
  "IBM Plex Mono":   `"IBM Plex Mono", ui-monospace, monospace`,
  "Fragment Mono":   `"Fragment Mono", ui-monospace, monospace`,
  "Space Mono":      `"Space Mono", ui-monospace, monospace`,
  "JetBrains Mono":  `"JetBrains Mono", ui-monospace, monospace`,
  "Tracked Sans":    `"Geist", -apple-system, sans-serif`,
};

// ─── Project data ────────────────────────────────────────────────────────────
const PROJECTS = [
  {
    id: "civic",
    name: "Civic Nexus",
    tag: "MCP made easy and safe.",
    items: [
      {
        kind: "screenshot",
        src: "civic/shot-03.png",
        h: "Connect every app your team uses.",
        em: "every app",
        p: "Nexus lets you securely connect your agent to tools. Select from >100 pre-built integrations, or build your own with the open-source SDK.",
      },
      {
        kind: "screenshot",
        src: "civic/shot-01.png",
        h: "Every call, audited and guardrailed.",
        em: "guardrailed",
        p: "Live dashboard for every tool call across every agent. Spot blocked attempts and failures before they become an incident — and drill into any row of the audit log without leaving the page.",
      },
      {
        kind: "screenshot",
        src: "civic/shot-06.png",
        h: "Bring your own agent.",
        em: "your own",
        p: "Claude Code, Codex, Cursor, OpenClaw, Vercel AI SDK — Civic plugs into whatever your team has chosen, not the other way around.",
      },
      {
        kind: "screenshot",
        src: "civic/shot-04.png",
        h: "A human in the loop, only when it matters.",
        em: "only when it matters",
        p: "Pending approvals queue up with the exact call, the requesting agent, and a one-click sign-off — or one-click block.",
      },
      {
        kind: "screenshot",
        src: "civic/shot-05.png",
        h: "Memory you can see.",
        em: "see",
        p: "Every shared fact your agents accumulate, tagged and searchable. Drill on a keyword to see what the swarm has remembered about it.",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/cCOZvRFVsSo",
        h: "Announcing Civic Nexus.",
        em: "Civic Nexus",
        p: "A short tour of the secure MCP gateway.",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/DOOq2NRBWE8",
        h: "Guardrails for peace of mind.",
        em: "peace of mind",
        p: "How Civic's guardrails work, and how they look in action when an agent tries to call a disallowed API.",
      },
    ],
  },
  {
    id: "rooney",
    name: "Rooney",
    tag: "The meeting assistant that gets things done.",
    items: [
      {
        kind: "screenshot",
        src: "rooney/shot-01.png",
        h: "Not another note-taker.",
        em: "Not another",
        p: "Rooney sits in your meetings, like any other employee, and has access to your organisation's resources. It will listen in while you talk and take actions on your behalf: filing tickets, updating Jira cards, and sending chat messages, without you having to lift a finger.",
      },
      {
        kind: "screenshot",
        src: "rooney/shot-10.png",
        h: "Two clicks of setup.",
        em: "Two clicks",
        p: "Integrates with your other agents using Civic Nexus",
      },
      {
        kind: "screenshot",
        src: "rooney/shot-03.png",
        h: "Auditable and replayable.",
        em: "Auditable",
        p: "Hedy lets you see why it made the decisions it did, and trace every action back to the exact moment in the meeting when it was taken.",
      },
      {
        kind: "screenshot",
        src: "rooney/shot-05.png",
        h: "Flexible to your business",
        em: "your business",
        p: "Give Rooney the tools your employees use, so and it will feel like part of the team.",
      },
      {
        kind: "screenshot",
        src: "rooney/shot-06.png",
        h: "Transparent breakdowns",
        em: "Transparent",
        p: "Per-tool P50, P95 and max latency, broken out across hundreds of calls. Spot the slow integration before anyone in the meeting does.",
      },
      {
        kind: "screenshot",
        src: "rooney/shot-08.png",
        h: "Transitions a ticket while you're still talking.",
        em: "still talking",
        p: "\"Rooney, move ticket 1232 to done.\" - Rooney handles the boring stuff while your team moves on.",
      },
    ],
  },
  {
    id: "hedy",
    name: "Hedy",
    tag: "Scalable engineering agent orchestrator.",
    items: [
      {
        kind: "screenshot",
        src: "hedy/shot-01.png",
        h: "Morning standups are a little different nowadays...",
        em: "little different",
        p: "Hedy is a round-the-clock, multitasking, engineering agent orchestrator. It can run any number of agents in parallel, and keep them all coordinated towards a shared goal.",
      },
      {
        kind: "screenshot",
        src: "hedy/shot-02.png",
        h: "Every run, replayable line by line.",
        em: "line by line",
        p: "Hedy keeps the full reasoning trace of every agent run — state assessment, action taken, code committed — linked back to the PR, the ticket and the security review.",
      },
    ],
  },
  {
    id: "canvas",
    name: "Canvas",
    tag: "Architecture diagrams, by talking.",
    items: [
      {
        kind: "screenshot",
        src: "canvas/shot-01.png",
        h: "A systems designer that can hear you.",
        em: "hear you",
        p: "Brainstorming a system design on a call? Canvas listens in, picks out the components, arrows and labels from your description, and turns them into a diagram in real time. No more scrambling for a whiteboard tool or trying to describe your vision with words alone.",
      },
      {
        kind: "video",
        src: "canvas/demo.mp4",
        h: "Watch a diagram build itself.",
        em: "build itself",
        p: "A captured session: Canvas listening to a system description and rendering the components, arrows and labels in step with the words.",
      },
      {
        kind: "screenshot",
        src: "canvas/shot-02.png",
        h: "Iterate on a system diagram out loud.",
        em: "out loud",
        p: "Hold space, describe a change — \"add a Redis cache\", \"make the worker fan out\" — and watch the diagram redraw. Every utterance lands in the debug log next to the resulting frame.",
      },
    ],
  },
  {
    id: "sunrise",
    name: "Sunrise Stake",
    tag: "Staking for climate.",
    items: [
      {
        kind: "screenshot",
        src: "sunrise/frame-01-54.png",
        h: "Your stake, your tree.",
        em: "your tree",
        p: "With Sunrise Stake, blockchain users can passively earn carbon offsets simply by staking",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/lnmawPasX8I",
        h: "Sunrise Stake — an intro.",
        em: "an intro",
        p: "How Sunrise turns SOL staking yield into retired carbon offsets, end-to-end on-chain.",
      },
      {
        kind: "screenshot",
        src: "sunrise/frame-03-00.png",
        h: "Connect with other stakers, and verified carbon projects",
        em: "verified carbon projects",
        p: "Trade with gSOL, the green staking token, and grow your forest, discovering new friends and exciting projects along the way.",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/TXr5anweDgI",
        h: "One transaction, end-to-end.",
        em: "end-to-end",
        p: "Stake, swap, mint the offset, and bridge the receipt back on-chain, all in a single signed message. No multi-step UX, no abandoned flows.",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/9-qLD1c-ImY",
        h: "The multi-chain Offset Bridge.",
        em: "Offset Bridge",
        p: "A guided tour of the protocol: how the yield streams retire offsets, where the receipts live on-chain, and what each number on the dashboard actually means.",
      },
    ],
  },
  {
    id: "solarium",
    name: "Solarium",
    tag: "Decentralised, end-to-end encrypted messenger.",
    items: [
      {
        kind: "video",
        src: "solarium/demo-quick.mp4",
        h: "Solarium - the first truly decentralised end-to-end encrypted messenger on Solana.",
        em: "truly decentralised",
        p: "Sign in with a wallet, pick a contact, send a message.",
      },
      {
        kind: "screenshot",
        src: "solarium/frame-09-24.png",
        h: "A chat that lives on no server.",
        em: "no server",
        p: "Channels, contacts, messages — routed via the Solana blockchain, with identities backed by Solana wallets and a verifiable key registry. There is no backend to compromise.",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/C_XVl1fT76Y",
        h: "Solarium, end-to-end.",
        em: "end-to-end",
        p: "A walkthrough of the messenger: spinning up a wallet-backed identity, finding a contact, sending an encrypted message — all backed by the Solana blockchain.",
      },
      {
        kind: "video",
        src: "https://www.youtube.com/embed/kgDRefJECIE",
        h: "A technical walkthrough.",
        em: "technical",
        p: "Watch the creators walk through the architecture and codebase of Solarium.",
      },
    ],
  },
];

// ─── Tile ────────────────────────────────────────────────────────────────────
function Tile({ item }) {
  if (item.kind === "video" && item.src) {
    const isYouTube = /youtube\.com|youtu\.be/.test(item.src);
    return (
      <div className="tile video embed">
        {isYouTube ? (
          <iframe
            src={item.src}
            title="Embedded video"
            frameBorder="0"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
            allowFullScreen
          />
        ) : (
          <video src={item.src} controls playsInline preload="metadata" />
        )}
      </div>
    );
  }
  if (item.kind === "screenshot" && item.src) {
    return (
      <div className="tile still embed">
        <img src={item.src} alt="" loading="lazy" />
      </div>
    );
  }
  return (
    <div className={`tile ${item.kind === "video" ? "video" : "still"}`}>
      <div className="tex"></div>
      {item.kind === "video" && (
        <>
          <div className="pulse"></div>
          <div className="play">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M8 5l12 7-12 7V5z"/>
            </svg>
          </div>
        </>
      )}
    </div>
  );
}

// ─── Highlight (no italics) ──────────────────────────────────────────────────
function withEm(text, em) {
  if (!em || !text.includes(em)) return text;
  const parts = text.split(em);
  return (
    <>
      {parts[0]}
      <span className="em">{em}</span>
      {parts.slice(1).join(em)}
    </>
  );
}

// ─── Project pane ────────────────────────────────────────────────────────────
function Pane({ project, nextId }) {
  const items = project.items;
  const max = Math.max(0, items.length - 1);
  const [pos, setPos] = useState(0);

  const advance = () => setPos(p => (p >= max ? 0 : p + 1));

  const total = items.length;
  const progDeg = max > 0 ? (pos / max) * 360 : 0;

  return (
    <section
      className="pane"
      id={project.id}
      data-screen-label={project.name}
    >
      <header className="pane-head">
        <h2>{withEm(project.name, project.em)}</h2>
        <p className="tag">{project.tag}</p>
      </header>

      <div className="stage">
        <div className="window">
          <div
            className="track"
            style={{ transform: `translateX(${-pos * 100}%)` }}
          >
            {items.map((it, i) => (
              <div className="slide" key={i}>
                <div className="slide-media">
                  <Tile item={it} />
                </div>
                <aside className="slide-caption">
                  {it.h && (
                    <>
                      <h3>{withEm(it.h, it.em)}</h3>
                      <p>{it.p}</p>
                    </>
                  )}
                </aside>
              </div>
            ))}
          </div>
        </div>

        <div className="plus-wrap">
          <button className="plus" onClick={advance} aria-label="Show next" disabled={max === 0}>
            <span className="ring" style={{ "--prog-deg": `${progDeg}deg` }}></span>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
              <path d="M12 4v16M4 12h16"/>
            </svg>
          </button>
          <span className="count">
            <span className="cur">{String(pos + 1).padStart(2, "0")}</span>
            <span> / {String(total).padStart(2, "0")}</span>
          </span>
        </div>
      </div>

      <footer className="pane-foot">
        {nextId && (
          <a className="more" href={`#${nextId}`}>
            <span>More</span>
            <svg className="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
              <path d="M6 9l6 6 6-6"/>
            </svg>
          </a>
        )}
      </footer>
    </section>
  );
}

// ─── App ─────────────────────────────────────────────────────────────────────
function App() {

  return (
    <>
      <header className="top">
        <div className="top-left">
          <a className="name" href="#civic">Daniel Kelleher</a>
          <span className="title">Portfolio</span>
        </div>
        <nav className="top-right">
          <a className="icon-link" href="https://www.linkedin.com/in/kelleherdaniel/" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" title="LinkedIn">
            <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
            </svg>
          </a>
          <a className="icon-link" href="https://danbkelleher.substack.com/" target="_blank" rel="noopener noreferrer" aria-label="Substack" title="Substack">
            <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z"/>
            </svg>
          </a>
          <a className="email" href="mailto:danielbkelleher@gmail.com">danielbkelleher@gmail.com</a>
        </nav>
      </header>
      <main>
        {PROJECTS.map((p, i) => (
          <Pane key={p.id} project={p} nextId={PROJECTS[i + 1]?.id} />
        ))}
      </main>
    </>
  );
}

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