Shipnoted

Docs

Vite

A single-page app: the widget is one line in index.html, and the hooks (or the framework-free core, for Vue and Svelte) give you the data.

  1. Create a project

    Sign in, create a project, and note its slug.

  2. Add the widget

    npx shipnoted init

    It adds one line to index.html, before </body>:

    index.html

    <script src="https://shipnoted.com/widget.js" data-project="your-slug" async></script>

React hooks#

npm i @shipnoted/react

src/Changelog.tsx

import { ShipnotedProvider, useChangelog, useFeedback } from "@shipnoted/react"

function Page() {
  const { releases, loading, unread, markSeen } = useChangelog({ limit: 10 })
  const { items, vote, hasVoted } = useFeedback({ status: "open", sort: "top" })
  if (loading) return <p>Loading…</p>
  return (
    <>
      <h1>What's new {unread > 0 && <b>({unread})</b>}</h1>
      {releases.map((r) => <article key={r.id}><h2>{r.title}</h2><p>{r.summary}</p></article>)}
      <h1>Vote</h1>
      {items.map((f) => <button key={f.id} aria-pressed={hasVoted(f.id)} onClick={() => vote(f.id)}>▲ {f.votes} {f.title}</button>)}
    </>
  )
}

export default () => <ShipnotedProvider project="your-slug" baseUrl="https://shipnoted.com"><Page /></ShipnotedProvider>

Vue, Svelte and others#

The data logic does not depend on React. @shipnoted/react/core exports plain stores with subscribe and getSnapshot, which every framework can bind to.

Svelte

import { createPublicClient, createChangelog } from "@shipnoted/react/core"

const log = createChangelog(createPublicClient({ project: "your-slug", baseUrl: "https://shipnoted.com" }), { limit: 10 })
log.refresh()
// a Svelte store is any object with subscribe(fn); wrap getSnapshot:
export const changelog = { subscribe(fn) { fn(log.getSnapshot()); return log.subscribe(() => fn(log.getSnapshot())) } }

Verified customers#

A browser-only app cannot sign tokens safely: the identify secret has to stay on a server. Add one small endpoint to your backend (any language: it is a standard HS256 JWT), then hand the token to the widget or the provider:

const { token } = await (await fetch("/api/shipnoted-token")).json()
window.Shipnoted?.identify(token)      // the widget
// <ShipnotedProvider userToken={token} …>  // the hooks