Skip to content
Merged

Main #15

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 115 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,72 @@

Your abandoned repos deserve a proper funeral.

**Live:** [commitmentissues.dev](https://commitmentissues.dev)
**Live:** [commitmentissues.dev](https://commitmentissues.dev)  ·  Built by [Dot Systems](https://github.com/dotsystemsdevs)

![MIT License](https://img.shields.io/github/license/dotsystemsdevs/commitmentissues?style=flat-square)
![Vercel Deploy](https://img.shields.io/badge/deployed%20on-Vercel-black?style=flat-square&logo=vercel)
[![MIT License](https://img.shields.io/github/license/dotsystemsdevs/commitmentissues?style=flat-square)](LICENSE)
[![Deployed on Vercel](https://img.shields.io/badge/deployed%20on-Vercel-black?style=flat-square&logo=vercel)](https://commitmentissues.dev)
[![Next.js](https://img.shields.io/badge/Next.js-14-black?style=flat-square&logo=next.js)](https://nextjs.org)

Paste a public GitHub URL. Get a shareable **Certificate of Death** — cause of death, last words, repo age, exportable graphics. No account required.
---

## Screenshots
Paste a public GitHub URL. Get a shareable **Certificate of Death** — algorithmic cause of death, last commit as last words, repo age, and exportable graphics. No signup. No account. Completely free.

## Embed your dead repo

Home:
Got a dead repo? Add the badge to your README:

![Homepage screenshot](docs/screenshots/homepage.png)
[![commitmentissues](https://img.shields.io/badge/%E2%9A%B0%20commitmentissues-declared%20dead-cc0000?style=for-the-badge&labelColor=1a0f06)](https://commitmentissues.dev)

Certificate:
```markdown
[![commitmentissues](https://img.shields.io/badge/%E2%9A%B0%20commitmentissues-declared%20dead-cc0000?style=for-the-badge&labelColor=1a0f06)](https://commitmentissues.dev/?repo=YOUR_OWNER/YOUR_REPO)
```

![Certificate screenshot](docs/screenshots/certificate.png)
Or embed the full certificate image:

About:
```markdown
[![Certificate of Death](https://commitmentissues.dev/api/certificate-image/YOUR_OWNER/YOUR_REPO)](https://commitmentissues.dev/?repo=YOUR_OWNER/YOUR_REPO)
```

Both are generated automatically on the certificate page — just hit **Copy** after analyzing your repo.

## Screenshots

![About page screenshot](docs/screenshots/about.png)
![Homepage](docs/screenshots/homepage.png)

![Certificate of Death](docs/screenshots/certificate.png)

![The Mortician — About page](docs/screenshots/about.png)

## Features

- **Certificate of Death** — A4-style layout with cause, last words, repo age, and derived stats
- **Exports** — Multiple aspect ratios (feed, square, story-style) for Instagram, X, Facebook
- **Mobile share** — Native share sheet on mobile with story-friendly format
- **Hall of Shame** — Curated leaderboard of famously abandoned repos
- **Recently Buried** — Live feed of the latest public burials
- **Chrome extension** — Tombstone badge injected on any GitHub repo page (MVP)
- **Certificate of Death** — A4 layout with cause of death, last words, repo age, stars, forks, and language
- **Algorithmic scoring** — `src/lib/scoring.ts` computes a death index from commit activity, archive status, issue count, and time since last push
- **Export** — PNG downloads in multiple aspect ratios: A4, Instagram (4:5 and 1:1), X/Twitter (16:9), Facebook feed, and Stories (9:16)
- **Mobile share** — Native share sheet on iOS/Android with a story-formatted image
- **README badge** — Embed a `⚰ DECLARED DEAD` shields.io badge linking back to the certificate
- **Certificate embed** — Full certificate image via `/api/certificate-image/[owner]/[repo]` for GitHub READMEs
- **Recently Buried** — Live scrolling feed of the latest public burials
- **Famous Casualties** — Curated graveyard of famously abandoned repos
- **Rate limiting** — Redis-backed per-IP limiting with graceful fallback
- **Timeout + race condition handling** — AbortController on every GitHub API call

## Tech stack

| | |
|---|---|
| Framework | Next.js 14 (App Router) |
| Styling | Tailwind CSS + inline styles |
| Fonts | UnifrakturMaguntia, Courier Prime, Inter |
| Export | `html-to-image`, Canvas API |
| Certificate image | `next/og` (Satori, Node.js runtime) |
| Hosting | Vercel |
| Storage | Upstash Redis (counters + recent burials) |
| Storage | Upstash Redis (rate limiting + recent burials + stats) |
| Data | GitHub public API |
| Analytics | Vercel Analytics + Plausible |

## Getting started

Prerequisites: Node 18+
**Prerequisites:** Node 18+

```bash
git clone https://github.com/dotsystemsdevs/commitmentissues.git
Expand All @@ -55,92 +78,110 @@ npm run dev

Open [http://localhost:3000](http://localhost:3000).

### Environment
### Environment variables

Add a GitHub token to raise API rate limits (optional but recommended):
Create a `.env.local` in the project root:

```env
# GitHub — optional but strongly recommended (raises API rate limits from 60 to 5000 req/hr)
GITHUB_TOKEN=ghp_yourtoken

# Upstash Redis — optional (enables Recently Buried feed, rate limiting, and buried counter)
KV_REST_API_URL=https://your-instance.upstash.io
KV_REST_API_TOKEN=your_token
```

Generate one at **GitHub → Settings → Developer settings → Personal access tokens**. Fine-grained or classic tokens both work.
**Without any env vars** the app still works fully — you just get GitHub's unauthenticated rate limits (60 req/hr) and the Recently Buried feed is hidden.

> **Note:** The Recently Buried feed requires Upstash Redis (`KV_REST_API_URL` + `KV_REST_API_TOKEN`). Without it, the feed is hidden and the buried counter falls back to the historical baseline.
Generate a GitHub token at **Settings → Developer settings → Personal access tokens**. Fine-grained or classic both work; no special scopes needed for public repo access.

## How we pronounce repos dead

| Step | What happens |
|------|----------------|
| Input | You submit a public GitHub URL |
| Data | The app fetches public metadata via the GitHub API |
| Score | A death index and narrative are computed in `src/lib/scoring.ts` |
| Output | Certificate rendered on-screen, exportable as PNG |

Hall of Shame entries are hand-curated; Recently Buried reflects real usage.

## Testing

```bash
npm test
```
|------|-------------|
| Input | User submits a public GitHub URL |
| Fetch | App fetches repo metadata + latest commit via GitHub API |
| Score | `computeDeathIndex()` in `src/lib/scoring.ts` produces a 0–10 death index |
| Narrative | `determineCauseOfDeath()` picks a cause based on the index and repo signals |
| Output | Certificate rendered client-side, exportable as high-res PNG |

## Contributing

- Read `.github/CONTRIBUTING.md` before opening a PR
- Use the issue and PR templates
- CI runs lint, tests, and build on pull requests to `master`
The scoring algorithm weighs: time since last commit, archive status, open issues, fork ratio, star count, and whether the last commit message reads like a final sigh.

## Project structure

```text
```
src/
├── app/
│ ├── page.tsx
│ ├── about/
│ ├── page.tsx ← homepage
│ ├── about/page.tsx ← /about
│ ├── layout.tsx ← root layout, fonts, analytics, JSON-LD
│ └── api/
│ ├── repo/
│ ├── stats/
│ └── recent/
│ ├── repo/route.ts ← main analysis endpoint
│ ├── recent/route.ts ← recently buried feed
│ ├── random/route.ts ← random dead repo picker
│ ├── stats/route.ts ← buried counter
│ ├── badge/[owner]/[repo]/ ← shields.io-compatible badge SVG
│ └── certificate-image/[owner]/[repo]/ ← OG image for README embeds
├── components/
│ ├── CertificateCard.tsx
│ ├── Leaderboard.tsx
│ ├── SearchForm.tsx
│ └── LoadingState.tsx
│ ├── CertificateCard.tsx ← certificate view + all export/share logic
│ ├── CertificateFixed.tsx ← the actual certificate layout (A4)
│ ├── SearchForm.tsx ← URL input + random button
│ ├── RecentlyBuried.tsx ← scrolling marquee feed
│ ├── Leaderboard.tsx ← Famous Casualties graveyard
│ ├── LoadingState.tsx ← loading skeleton
│ ├── ErrorDisplay.tsx ← error + retry UI
│ ├── PageHero.tsx ← shared hero (emoji, title, subtitle)
│ ├── SubpageShell.tsx ← shell for /about and future subpages
│ └── SiteFooter.tsx ← footer
└── lib/
├── scoring.ts
├── rateLimit.ts
├── recentStore.ts
── types.ts
extension/ ← Chrome extension (MV3, load unpacked)
├── scoring.ts ← death index + cause of death logic
├── scoring.test.ts ← scoring unit tests
├── rateLimit.ts ← Redis-backed rate limiting
── recentStore.ts ← recently buried Redis store
└── types.ts ← shared TypeScript types
```

## Chrome extension (MVP)
## Running tests

A MV3 extension lives under `extension/`. It injects a tombstone badge on GitHub repo pages and links to the full certificate.
```bash
npm test
```

Tests cover the scoring algorithm in `src/lib/scoring.test.ts`.

### Load unpacked in Chrome
## Contributing

Contributions are welcome. Please read [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) before opening a PR.

1. Open `chrome://extensions/`
2. Enable **Developer mode**
3. Click **Load unpacked** and select the `extension/` subfolder
- Use the issue templates for bugs and feature requests
- CI runs lint, tests, and build on every pull request to `master`
- Keep PRs focused — one thing at a time

### Test flow
## Roadmap

1. Open a GitHub repo page (e.g. `https://github.com/vercel/next.js`)
2. Verify a tombstone badge appears near the repo header
3. Click the badge to open the full certificate on `commitmentissues.dev`
4. Navigate to another repo without a full reload; verify no duplicate badge appears
Items are loosely prioritized. Community PRs welcome on anything marked **good first issue**.

If the API is rate-limited or unavailable, the badge falls back to `Reaper busy`.
### Near-term
- [ ] Upgrade to Next.js 16 (planned within one month of launch)
- [ ] Dark mode
- [ ] `/api/certificate-image` caching layer (currently no Redis cache)
- [ ] Repo comparison — bury two repos side by side

## Docs
### Longer-term
- [ ] Chrome extension — tombstone badge injected on GitHub repo pages
- [ ] Language-specific causes of death ("Died of PHP fatigue", "Last seen in CoffeeScript")
- [ ] Death anniversary emails — opt-in reminders on the date of last commit
- [ ] API for third-party integrations

- Release notes: `docs/releases/`
- Screenshots: `docs/screenshots/`
- Repository conventions: `docs/repository-conventions.md`
### Won't do (by design)
- Private repo support — we don't break into houses
- Accounts / login — the funeral is free and anonymous
- Monetization — coffee button stays, paywalls don't

## License

MIT — see repository license file.
MIT — see [`LICENSE`](LICENSE).

---

Built by [Dot Systems](https://github.com/dotsystemsdevs).
Built with too much free time by [Dot Systems](https://github.com/dotsystemsdevs). If it made you laugh, [keep us alive](https://buymeacoffee.com/commitmentissues).
3 changes: 3 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Living document. Completed work at the top, upcoming at the bottom. Add new entr
- OG image aligned with live hero (light theme, correct fonts)
- Custom roast line for commitmentissues repo itself

### Bug fixes
- Fixed "bury something else →" on error screen — was retrying same broken URL instead of resetting to the form

### Data integrity
- Audited all 28 Famous Casualties graveyard entries against live GitHub API
- Fixed `angularjs/angular.js` → `angular/angular.js` (org transferred, old path 404)
Expand Down
12 changes: 12 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ const nextConfig = {
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: blob: https://img.shields.io https://avatars.githubusercontent.com",
"connect-src 'self' https://vitals.vercel-insights.com https://va.vercel-scripts.com",
"frame-ancestors 'none'",
].join('; '),
},
],
},
]
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import type { Metadata } from 'next'
import SubpageShell from '@/components/SubpageShell'

export const metadata: Metadata = {
title: 'About — commitmentissues',
description: 'How commitmentissues works: real GitHub data, a death score algorithm, and a healthy dose of dark humor for your abandoned repos.',
title: 'About — Commitment Issues | How It Works',
description: 'How Commitment Issues works: we analyze your GitHub repo\'s commit history, activity decay, and open issues to assign a cause of death and generate a printable death certificate.',
alternates: { canonical: 'https://commitmentissues.dev/about' },
openGraph: {
title: 'About — commitmentissues',
description: 'How commitmentissues works: real GitHub data, a death score algorithm, and a healthy dose of dark humor for your abandoned repos.',
title: 'About — Commitment Issues | How It Works',
description: 'How Commitment Issues works: we analyze your GitHub repo\'s commit history, activity decay, and open issues to assign a cause of death and generate a printable death certificate.',
url: 'https://commitmentissues.dev/about',
},
}
Expand Down Expand Up @@ -62,7 +62,7 @@ export default function AboutPage() {
padding: '20px 18px',
border: '2px solid #1a1a1a',
borderRadius: '0',
background: '#f2f2f2',
background: '#EDE8E1',
}}
>
<p style={{ fontFamily: UI, fontSize: 'clamp(16px, 4.2vw, 17px)', fontWeight: 700, color: '#160A06', margin: '0 0 10px 0' }}>
Expand Down
Loading
Loading