···11+# Plan: Add Patreon OAuth Login to Sponsor Panel
22+33+## Context
44+55+The sponsor-panel (`cmd/sponsor-panel/`) currently only supports GitHub OAuth login. Patreon patrons have no way to access sponsor benefits (Discord invite, team invitations, logo submissions). This change adds Patreon as a second authentication provider so patrons get feature parity with GitHub Sponsors.
66+77+**Design decisions:**
88+99+- User-token based verification (no saasproxy dependency)
1010+- Separate identities (no account linking between GitHub and Patreon)
1111+- Patreon API v2 via direct HTTP calls; `patreon-go` library used only for OAuth URL constants
1212+- OAuth scopes: `identity`, `identity[email]`, `campaigns.members`
1313+- Team invite: Patreon $50+ users see the same card and input a GitHub username
1414+1515+---
1616+1717+## Step 1: Database Migration
1818+1919+**File:** `cmd/sponsor-panel/migrations.go`
2020+2121+Add `migration002` constant with idempotent DDL:
2222+2323+- `ALTER TABLE users ALTER COLUMN github_id DROP NOT NULL`
2424+- `ADD COLUMN IF NOT EXISTS patreon_id TEXT UNIQUE`
2525+- `ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'github'`
2626+- Drop `users_login_key` unique constraint, replace with `UNIQUE INDEX (provider, login)`
2727+- Add `idx_users_patreon_id` index
2828+2929+Update `runMigrations()` to execute `migration002` after `migrationSchema`.
3030+3131+---
3232+3333+## Step 2: Update User Model
3434+3535+**File:** `cmd/sponsor-panel/models.go`
3636+3737+- Change `User.GitHubID` from `int64` to `*int64` (nullable)
3838+- Add `PatreonID *string` and `Provider string` fields
3939+- Update all `SELECT`/`Scan` calls in `getUserByID`, `upsertUser` to include new columns
4040+- Add `upsertPatreonUser(ctx, pool, user)` — same pattern as `upsertUser` but upserts by `patreon_id`, sets `provider='patreon'`
4141+4242+---
4343+4444+## Step 3: Add Patreon OAuth Config
4545+4646+**File:** `cmd/sponsor-panel/main.go`
4747+4848+New flags (all optional — service still works GitHub-only if omitted):
4949+5050+- `--patreon-client-id`
5151+- `--patreon-client-secret`
5252+- `--patreon-redirect-url`
5353+- `--patreon-campaign-id` (to match pledge against)
5454+5555+Add to `Server` struct:
5656+5757+```go
5858+patreonOAuth *oauth2.Config // nil if not configured
5959+patreonCampaignID string
6060+```
6161+6262+In `main()`, conditionally create `oauth2.Config` using `patreon.AuthorizationURL` and `patreon.AccessTokenURL` from `gopkg.in/mxpv/patreon-go.v1`.
6363+6464+Register new routes:
6565+6666+```
6767+/login/patreon → server.patreonLoginHandler
6868+/callback/patreon → server.patreonCallbackHandler
6969+```
7070+7171+---
7272+7373+## Step 4: Patreon OAuth Handlers (new file)
7474+7575+**File:** `cmd/sponsor-panel/patreon_oauth.go` (new)
7676+7777+### `patreonLoginHandler`
7878+7979+Mirrors `loginHandler` in `oauth.go`: generate state, set CSRF cookie, redirect to `s.patreonOAuth.AuthCodeURL(state)`. Returns 404 if `s.patreonOAuth == nil`.
8080+8181+### `patreonCallbackHandler`
8282+8383+1. Validate state cookie (same CSRF pattern as GitHub)
8484+2. Exchange code via `s.patreonOAuth.Exchange(ctx, code)`
8585+3. Call Patreon API v2 identity endpoint:
8686+ ```
8787+ GET https://www.patreon.com/api/oauth2/v2/identity
8888+ ?include=memberships.campaign
8989+ &fields[user]=full_name,vanity,email,image_url
9090+ &fields[member]=patron_status,currently_entitled_amount_cents
9191+ ```
9292+4. Parse JSON:API response to extract user identity and membership data
9393+5. Find membership matching `s.patreonCampaignID`
9494+6. Build `SponsorshipData` JSON: `{is_active, monthly_amount_cents, tier_name}`
9595+7. Call `upsertPatreonUser()` with `provider="patreon"`, login = vanity or full_name
9696+8. Create session (same `user_id` in gorilla/sessions cookie)
9797+9. Redirect to `/`
9898+9999+### Response types (define in same file):
100100+101101+- `patreonIdentityResponse` — JSON:API envelope with user data + included memberships
102102+- `patreonMember` — membership attributes (patron_status, currently_entitled_amount_cents) + campaign relationship
103103+104104+---
105105+106106+## Step 5: Update Login Template
107107+108108+**File:** `cmd/sponsor-panel/templates/login.templ`
109109+110110+Change signature to `templ Login(patreonEnabled bool)`. Add a "Login with Patreon" button (with Patreon SVG icon) conditionally rendered when `patreonEnabled` is true, linking to `/login/patreon`.
111111+112112+---
113113+114114+## Step 6: Update Dashboard for Provider Awareness
115115+116116+**File:** `cmd/sponsor-panel/templates/dashboard.templ`
117117+118118+- Add `Provider string` to `UserProps`
119119+- In `SponsorshipCard`, when user is not a sponsor: show Patreon link for `provider == "patreon"`, GitHub Sponsors link otherwise
120120+121121+**File:** `cmd/sponsor-panel/dashboard.go`
122122+123123+- `loginPageHandler`: pass `s.patreonOAuth != nil` to `templates.Login()`
124124+- `dashboardHandler`: set `UserProps.Provider` from `user.Provider`
125125+126126+---
127127+128128+## Step 7: Generate & Build
129129+130130+1. `go tool templ generate` (regenerate `*_templ.go` files)
131131+2. `go build ./cmd/sponsor-panel/`
132132+3. `npm test` (`go test ./...`)
133133+134134+---
135135+136136+## Files Modified
137137+138138+| File | Change |
139139+| --------------------------------------------- | ----------------------------------------------- |
140140+| `cmd/sponsor-panel/migrations.go` | Add migration002 |
141141+| `cmd/sponsor-panel/models.go` | Update User struct, add upsertPatreonUser |
142142+| `cmd/sponsor-panel/main.go` | Add flags, Server fields, routes |
143143+| `cmd/sponsor-panel/patreon_oauth.go` | **New** — Patreon login/callback handlers |
144144+| `cmd/sponsor-panel/oauth.go` | No changes (existing GitHub flow untouched) |
145145+| `cmd/sponsor-panel/dashboard.go` | Pass patreonEnabled and Provider |
146146+| `cmd/sponsor-panel/templates/login.templ` | Add Patreon button |
147147+| `cmd/sponsor-panel/templates/dashboard.templ` | Add Provider to props, conditional sponsor link |
148148+149149+## Existing Code to Reuse
150150+151151+- `generateState()` in `oauth.go:20` — reuse for Patreon CSRF state
152152+- `SponsorshipData` struct in `models.go:27` — same JSON format for both providers
153153+- `User.IsSponsorAtTier()` in `models.go:35` — works provider-agnostically
154154+- Session management via `getSessionUser()` in `oauth.go:634` — no changes needed
155155+- `patreon.AuthorizationURL` / `patreon.AccessTokenURL` from `gopkg.in/mxpv/patreon-go.v1`
156156+- All feature handlers (`inviteHandler`, `logoHandler`) — work unchanged since they only check `IsSponsorAtTier()`
157157+158158+## Verification
159159+160160+1. **Build**: `go build ./cmd/sponsor-panel/` compiles without errors
161161+2. **Tests**: `npm test` passes
162162+3. **GitHub flow unchanged**: Login with GitHub still works identically
163163+4. **Patreon login**: With Patreon OAuth credentials set, clicking "Login with Patreon" redirects to Patreon, callback creates user with `provider=patreon`
164164+5. **Tier gating**: A Patreon user pledging $50+/month sees team invite card; $1+ sees logo submission and Discord
165165+6. **No Patreon config**: When Patreon flags are omitted, login page only shows GitHub button, `/login/patreon` returns 404
···11+# Replace GitHub Sponsor iframe with Styled Sponsor Card
22+33+## Context
44+55+The homepage (`lume/src/index.jsx` line 40) currently embeds a GitHub Sponsors iframe. This should be replaced with a custom-styled card linking to Patreon, GitHub Sponsors, and the Sponsor Panel (sponsors.xeiaso.net). The card design should echo the sponsor-panel app's warm Gruvbox aesthetic.
66+77+## File to modify
88+99+- `lume/src/index.jsx` — the only file that needs changes
1010+1111+## Design reference (from `cmd/sponsor-panel`)
1212+1313+- Cards: `rounded-2xl`, border, surface bg, `overflow-hidden`, subtle shadow
1414+- Gradient accent line: 2px at top, orange-to-pink for warm variant
1515+- Card titles: `font-serif`, semibold
1616+- Buttons: `rounded-xl`, colored bg, hover lift (`hover:-translate-y-px`), white text
1717+1818+## Plan
1919+2020+### 1. Add small icon components before the default export
2121+2222+Copy SVG paths from `lume/src/donate.jsx` (GitHubIcon, PatreonIcon) scaled to 20x20. Add a star icon for the Sponsor Panel link.
2323+2424+### 2. Add `SponsorCard` component
2525+2626+Structure:
2727+2828+```
2929+<div> — card container (rounded-2xl, border, bg-bg-2, dark:bg-bgDark-2, shadow-sm, overflow-hidden, max-w-xl, mx-auto, my-6)
3030+ <div> — gradient accent line (2px, orange→purple, with dark mode variant via dark:hidden/hidden dark:block)
3131+ <div> — content area (px-6 py-5 text-center)
3232+ <h3> — "Support My Work" with heart SVG icon
3333+ <p> — brief description (text-sm, muted color)
3434+ <div> — flex row of 3 button-links (flex-wrap, gap-3, centered)
3535+ <a> Patreon — bg-orange-light / dark:bg-orangeDark-light
3636+ <a> GitHub Sponsors — bg-fg-0 / dark:bg-purpleDark-light
3737+ <a> Sponsor Panel — bg-purple-light / dark:bg-blueDark-light
3838+```
3939+4040+### 3. Replace the iframe (line 40) with `<SponsorCard />`
4141+4242+## Key implementation details
4343+4444+**Gradient accent line:** Use two `<div>`s with `dark:hidden` / `hidden dark:block` to swap light/dark gradients, since inline `style` can't respond to `prefers-color-scheme`:
4545+4646+- Light: `linear-gradient(90deg, #d65d0e, #b16286)` (orange→purple)
4747+- Dark: `linear-gradient(90deg, #fe8019, #d3869b)` (bright orange→pink)
4848+4949+**Button link style overrides:** The site's base `<a>` styles (in `lume/src/styles.css` line 61-63 and `hack.css` line 65-71) apply link colors, underline, border-bottom, and visited styles. Each button `<a>` needs:
5050+5151+- `no-underline border-0` to remove text decoration and bottom border
5252+- `text-white hover:text-white hover:bg-[color]` to override link/hover colors
5353+- `visited:text-white visited:hover:text-white visited:hover:bg-[color]` to override visited states
5454+5555+**Color tokens available** (from `lume/tailwind.config.js`):
5656+5757+- `bg-orange-light`, `dark:bg-orangeDark-light`, `hover:bg-orange-dark`
5858+- `bg-purple-light`, `dark:bg-blueDark-light`, `hover:bg-purple-dark`
5959+- `bg-fg-0`, `dark:bg-purpleDark-light`, `hover:bg-fg-1`
6060+- `text-fg-0`, `dark:text-fgDark-1`, `text-fg-3`, `dark:text-fgDark-3`
6161+6262+## Verification
6363+6464+1. Run `npm run dev` and check the homepage in both light and dark mode
6565+2. Verify all three links open correctly in new tabs
6666+3. Confirm the card is responsive (shrinks gracefully on mobile)
6767+4. Check that button hover states work (lift effect, color change, no link underline artifacts)