# Share card

`renderShareCard` draws the whole year on one card and gives it back as a PNG. It is painted on a canvas, so the file is the same on every machine: 1200 by 630, saved at twice that. Use it for the picture people post, pin to a profile or drop into a README.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/graphics/share-card/

## Installation

```bash
pnpm add @react-wrapped/graphics @react-wrapped/core gsap
```

## Usage

Call it with a spec and a palette. It needs a browser, so call it from an effect or a click. The component below draws the card once per palette and shows it with a download link.

`share-card.tsx`

```tsx
import { useEffect, useState } from "react";

import { resolvePalette, type WrappedConfig } from "@react-wrapped/core";
import { renderShareCard, type ShareCardSpec } from "@react-wrapped/graphics";

export function ShareCard({
  spec,
  config,
  fileName,
}: {
  spec: ShareCardSpec;
  config: WrappedConfig;
  fileName: string;
}) {
  const [url, setUrl] = useState<string | null>(null);
  const [failed, setFailed] = useState(false);
  const { palette, accent, mode } = config;

  // Drawn on a canvas, so only in the browser, and again whenever the palette changes.
  useEffect(() => {
    let made: string | null = null;
    let gone = false;
    const draw = async () => {
      try {
        const png = await renderShareCard(spec, resolvePalette({ palette, accent, mode }));
        if (gone) return;
        made = URL.createObjectURL(png);
        setUrl(made);
      } catch {
        setFailed(true);
      }
    };
    void draw();
    return () => {
      gone = true;
      if (made) URL.revokeObjectURL(made);
    };
  }, [spec, palette, accent, mode]);

  if (failed) return <p>The card could not be drawn in this browser.</p>;
  if (!url) return <p>Drawing the card.</p>;
  return (
    <div>
      <img src={url} alt={`@${spec.handle}, ${spec.caption}`} width={1200} height={630} />
      <div role="group" aria-label="Share card">
        <a href={url} download={fileName}>
          Download the PNG
        </a>
      </div>
    </div>
  );
}
```

## The spec

| Key | Type | What it is |
| --- | --- | --- |
| `handle` | `string` | Written as @handle next to the avatar. |
| `avatarUrl` | `string \| null` | The picture. With `null`, or when it does not load, the handle's first two letters on a disc. |
| `caption` | `string` | What this Wrapped is called. Set under the handle, before the period. |
| `period` | `string` | The window the numbers cover, already formatted. |
| `graph?` | `{ levels: readonly number[]; lead: number }` | The year as cells, the same shape as a scene's calendar. Leave it out and the numbers move up. |
| `stats` | `readonly { label: string; value: string }[]` | The headline numbers, already formatted. Up to four share the row. |
| `tokens?` | `readonly WrappedToken[]` | What this person got, as tinted plates at the bottom. The first two are drawn. |
| `brand?` | `string` | A quiet line in the top right: a site address or a product name. |

```ts
import type { ShareCardSpec } from "@react-wrapped/graphics";
import { PIXEL_ART } from "@react-wrapped/core";

export const card: ShareCardSpec = {
  handle: "ada",
  avatarUrl: null,
  caption: "Orbit Wrapped",
  period: "Oct 2025 to Oct 2026",
  stats: [
    { label: "Contributions", value: "2,052" },
    { label: "Longest streak", value: "48 days" },
    { label: "Biggest day", value: "58" },
    { label: "Top language", value: "TypeScript" },
  ],
  tokens: [{ name: "The Marathoner", kind: "Coder type", rarity: "epic", art: PIXEL_ART.mountain }],
  brand: "orbit.example",
};
```

## What it draws

- The palette's background with one soft light in the top left corner, as on the video.
- The avatar or the initials, the handle, and the caption with the period after it.
- The year as cells, when you pass `graph`.
- The numbers in one row. A value too long for its column is set smaller until it fits.
- Up to two tokens at the bottom, each tinted in its rarity colour with its pixel art.

> The card is set in Inter and waits for the page's fonts before it draws. Load Inter on the page, or the text falls back to the system font. An avatar from another site has to allow cross-origin use, or the card draws the initials.

## As a scene

The same content closes the video as the [summary scene](https://bettertyped.github.io/react-wrapped/docs/scenes/summary/). In the [gallery](https://bettertyped.github.io/react-wrapped/docs/graphics/gallery/) the card is one tile: hand it `image: () => renderShareCard(card, palette)`.

## Related

- [Graphics](https://bettertyped.github.io/react-wrapped/docs/graphics/)
- [Insight cards](https://bettertyped.github.io/react-wrapped/docs/graphics/insight-cards/)
- [Graphics gallery](https://bettertyped.github.io/react-wrapped/docs/graphics/gallery/)
- [Summary scene](https://bettertyped.github.io/react-wrapped/docs/scenes/summary/)

Source: https://bettertyped.github.io/react-wrapped/docs/graphics/share-card/
