Repository: https://github.com/HarshalPatel1972/GitFit.git
Tagline: Your GitHub, finally under control.
Version: Full Spec (All Phases)
Author: Harshal Patel
- Atomic commits only. Every commit = one logical unit of work. Never bundle unrelated changes. Commit message format:
type(scope): description— e.g.feat(auth): add GitHub OAuth with NextAuth v5,fix(bulk): handle rate limit on archive,style(dashboard): refine repo card hover state. Types:feat,fix,style,refactor,chore,docs,test. - Never commit broken code. Every commit must leave the app in a runnable state.
- Feature branches. Branch per feature:
feat/bulk-actions,feat/stars-manager,feat/pins-editor. Merge tomainwhen feature is complete and stable. - Read this entire document before writing a single line of code.
- Environment variables go in
.env.localonly. Never hardcode secrets. Always update.env.examplewhen adding a new env var. - No placeholder UIs. Every screen built must be functional and match the design system. No lorem ipsum. No grey boxes.
- Mobile-first responsive. Every component must work at 320px and scale up gracefully.
- Optimistic UI everywhere. Mutations update the UI instantly; API fires in background. On error, revert and toast.
| Field | Value |
|---|---|
| Name | GitFit |
| Domain | gitfit.vercel.app (or custom) |
| Repo | https://github.com/HarshalPatel1972/GitFit.git |
| Stack | Next.js 14 (App Router), NextAuth v5, Octokit, TanStack Query, Tailwind CSS |
| Deploy | Vercel (free tier, no credit card) |
| Database | None — GitHub API is the source of truth |
| Auth | GitHub OAuth only |
GitFit's visual language is a warm, editorial blend of Claude's calm intelligence and GitHub's developer trust. It should feel like someone took GitHub's information density and gave it a cozy reading-lamp glow — the kind of dashboard you want to open because it feels considered, warm, and human.
The aesthetic is: Refined Warmth × Developer Precision
- Not cold and sterile (GitHub default)
- Not garish or loud
- Warm, legible, intentional — like a beautifully typeset developer's journal
:root {
/* Core Backgrounds */
--bg-canvas: #0d0b09; /* near-black with warm undertone — main app bg */
--bg-surface: #161310; /* card/panel background */
--bg-elevated: #1e1a16; /* modals, dropdowns, raised elements */
--bg-hover: #252018; /* hover state for interactive rows */
/* Warm Neutrals */
--border-subtle: #2e2820; /* hairline borders */
--border-default: #3d3528; /* default borders */
--border-strong: #5a4d3a; /* focus rings, active borders */
/* Text */
--text-primary: #f0ebe3; /* headings, primary content */
--text-secondary: #b5a898; /* secondary labels, metadata */
--text-muted: #7a6e63; /* placeholders, disabled */
--text-inverse: #0d0b09; /* text on warm accent */
/* Accent — Warm Amber (primary action) */
--accent-primary: #d4a843; /* buttons, active states, CTAs */
--accent-primary-dim: #a07d28; /* hover on primary */
--accent-glow: rgba(212, 168, 67, 0.15); /* glow for focus rings */
/* Accent — Terracotta (destructive, delete) */
--accent-danger: #c0522a;
--accent-danger-dim: #8f3b1c;
--accent-danger-glow: rgba(192, 82, 42, 0.15);
/* Accent — Sage Green (success, public) */
--accent-success: #6b9e72;
--accent-success-dim: #4f7a56;
/* Accent — Slate Blue (archive, neutral action) */
--accent-neutral: #6b83a0;
--accent-neutral-dim: #4f6278;
/* Semantic states */
--repo-public: var(--accent-success);
--repo-private: var(--accent-neutral);
--repo-archived: var(--text-muted);
--repo-forked: #a07d28;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4);
--shadow-md: 0 4px 16px rgba(0,0,0,0.5);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.6);
--shadow-glow: 0 0 0 3px var(--accent-glow);
}Primary Font: Fraunces (variable, from Google Fonts)
Use @import in globals.css:
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&display=swap');/* Type Scale */
--font-display: 'Fraunces', Georgia, serif; /* headings, logo, hero text */
--font-body: 'Fraunces', Georgia, serif; /* body copy — Fraunces is legible enough */
--font-mono: 'JetBrains Mono', 'Fira Code', monospace; /* code, repo names, counts */
/* Sizes */
--text-xs: 0.75rem; /* 12px — labels, badges */
--text-sm: 0.875rem; /* 14px — meta, secondary */
--text-base: 1rem; /* 16px — body default */
--text-lg: 1.125rem; /* 18px — card titles */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px — section headers */
--text-3xl: 1.875rem; /* 30px — page titles */
--text-4xl: 2.25rem; /* 36px — hero */
/* Weights */
--weight-light: 300;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold:600;
--weight-bold: 700;
--weight-black: 900;Font Personality Rules:
- Repo names →
font-mono(they are identifiers) - All headings → Fraunces, weight 600–800, optical size 144
- Stats/counts → Fraunces italic, weight 300–400 (elegant, airy)
- Action labels (button text) → Fraunces, weight 600, tracking
+0.02em - Metadata (dates, sizes) → Fraunces, weight 300, muted color
Base unit: 4px
Layout max-width: 1280px
Sidebar width: 240px
Content padding: 24px (desktop), 16px (mobile)
Card gap: 16px
Section gap: 48px
/* Border radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Transitions */
--transition-fast: 120ms ease;
--transition-base: 200ms ease;
--transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);Repo Card
- Background:
--bg-surface - Border:
1px solid --border-subtle - Border-radius:
--radius-lg - Padding:
16px 20px - On hover: border transitions to
--border-default,translateY(-1px), subtle shadow - Checkbox appears on hover (top-left) OR when any card is selected (all cards show checkbox)
- Selection state:
--border-strongborder,--accent-glowbox-shadow - Repo name:
font-mono,--text-base,--text-primary - Description: Fraunces,
--text-sm,--text-secondary, 2-line clamp - Meta row: language dot + name, star count, fork count, last pushed — Fraunces light,
--text-muted - Badges:
public(sage pill),private(slate pill),archived(grey pill),fork(amber pill)
Bulk Action Bar (floats at bottom when ≥1 repo selected)
- Fixed, bottom-center,
translateY(100%)→translateY(0)spring animation on appear - Background:
--bg-elevated, backdrop-blur, border--border-strong - Border-radius:
--radius-xl - Padding:
12px 20px - Contents: "N selected" label | Divider | [Archive] [Privatize] [Publicize] [Tag] [Rename] [Delete]
- Delete button: terracotta color, separated by divider from other actions
- Each button: icon + label,
--radius-md, hover fills with action accent color - Dismiss: click outside selected repos, or press Escape
Filter Bar
- Sticky below the header
- Horizontal scroll on mobile (no wrapping)
- Filters: Search (full-text across name+description), Visibility (All / Public / Private), Status (All / Active / Archived), Language (dropdown, all unique languages detected), Sort (Updated / Name / Stars / Size / Created)
- Each filter pill:
--bg-elevated, active state fills with--accent-primaryat 20% opacity + amber border
Toast Notifications
- Top-right, stacked
- Success: sage green left border
- Error: terracotta left border
- Undo toasts: include countdown bar + "Undo" button (3s window for reversible actions)
gitfit/
├── .github/
│ └── copilot-instructions.md ← agent operating rules (copy from Section 0)
├── app/
│ ├── layout.tsx ← root layout, fonts, providers
│ ├── page.tsx ← landing page (unauthenticated)
│ ├── (auth)/
│ │ └── api/auth/[...nextauth]/route.ts
│ ├── (app)/
│ │ ├── layout.tsx ← authenticated layout with sidebar
│ │ ├── dashboard/
│ │ │ └── page.tsx ← main repo manager
│ │ ├── stars/
│ │ │ └── page.tsx ← stars manager
│ │ ├── feed/
│ │ │ └── page.tsx ← cross-repo PR + issue feed
│ │ ├── pins/
│ │ │ └── page.tsx ← pin editor
│ │ └── settings/
│ │ └── page.tsx ← app preferences
├── components/
│ ├── ui/ ← base components (Button, Badge, Toast, Modal, Input)
│ ├── repo/
│ │ ├── RepoCard.tsx
│ │ ├── RepoGrid.tsx
│ │ ├── FilterBar.tsx
│ │ ├── BulkActionBar.tsx
│ │ └── BulkDeleteConfirm.tsx
│ ├── stars/
│ │ ├── StarCard.tsx
│ │ └── StarGrid.tsx
│ ├── feed/
│ │ ├── FeedItem.tsx
│ │ └── FeedList.tsx
│ ├── pins/
│ │ └── PinEditor.tsx
│ └── layout/
│ ├── Sidebar.tsx
│ ├── Header.tsx
│ └── UserMenu.tsx
├── lib/
│ ├── github/
│ │ ├── client.ts ← Octokit REST client factory
│ │ ├── graphql.ts ← GitHub GraphQL client factory
│ │ ├── repos.ts ← repo fetch helpers
│ │ ├── stars.ts ← stars fetch helpers
│ │ ├── feed.ts ← PRs + issues fetch helpers
│ │ └── pins.ts ← pin GraphQL queries
│ └── actions/
│ ├── bulk.ts ← bulkPrivatize, bulkArchive, bulkDelete, bulkTag, bulkRename
│ ├── stars.ts ← unstar, bulk unstar
│ └── pins.ts ← updatePins
├── hooks/
│ ├── useRepos.ts
│ ├── useSelection.ts
│ ├── useStars.ts
│ └── useFeed.ts
├── types/
│ └── index.ts ← GitFitRepo, GitFitStar, FeedItem, Pin types
├── .env.example
├── .env.local ← NEVER commit
└── tailwind.config.ts
# .env.example
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET= # openssl rand -base64 32
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# GitHub OAuth App settings:
# Homepage URL: http://localhost:3000
# Callback URL: http://localhost:3000/api/auth/callback/githubRequired OAuth Scopes (set in NextAuth config):
repo # Read + write all repos (public + private)
delete_repo # Separate scope required for deletion
user # Profile, avatar, username
read:org # Phase 3 — org repos
Shown only to unauthenticated users. Authenticated users redirect to /dashboard.
Layout:
- Full-viewport hero
- Background:
--bg-canvaswith a very subtle warm noise texture (CSSfilter: url(#noise)SVG filter orbackground-image: url(/noise.png)) - Centered content, generous vertical padding
Hero Content:
[GitFit logotype — Fraunces Black, --text-4xl, warm amber]
Your GitHub,
finally under control.
[Fraunces Light Italic, --text-3xl, --text-secondary, 2 lines]
[Sign in with GitHub — primary button, full icon + label]
Feature Grid (below hero, 3-column):
🗂 Bulk Actions — Archive, privatize, delete — 10 repos in 10 seconds.
🔍 Smart Filters — Dead repos, by language, by age. Find anything instantly.
⭐ Stars Manager — Browse and prune your 400 forgotten starred repos.
📌 Pin Editor — Drag, reorder, publish. Visual pin management.
🔀 Cross-repo Feed — All your PRs and issues. One dashboard.
🏷 Bulk Tagging — Add topics to 20 repos in one click.
Footer: "Open source. No data stored. Reads and writes only through GitHub's official API."
Use NextAuth v5 (Auth.js).
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorization: {
params: {
scope: "repo delete_repo user read:org"
}
}
})
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
}
return token
},
async session({ session, token }) {
session.accessToken = token.accessToken as string
return session
}
}
})Store accessToken on the session. Every server action reads it via await auth().
Sidebar (240px, fixed left):
[GitFit logo — top, links to /dashboard]
─────────────────────
Dashboard (grid icon)
Stars (star icon)
Feed (activity icon)
Pins (pin icon)
─────────────────────
Settings (gear icon)
─────────────────────
[Avatar + username]
[Sign out]
Sidebar background: --bg-surface. Active item: amber left border (3px solid --accent-primary), --bg-hover fill.
Main content area: calc(100vw - 240px), scrollable, --bg-canvas.
Mobile: Sidebar collapses to hamburger. Slides in from left as overlay.
Page Header:
Your Repositories
[Fraunces, --text-3xl] [Refresh button]
[X public] [Y private] [Z archived] [total: N]
[stat pills, Fraunces Light Italic]
Filter Bar (sticky):
[🔍 Search repos...] [Visibility ▾] [Status ▾] [Language ▾] [Sort ▾] [View: Grid|List]
Repo Grid: CSS Grid, repeat(auto-fill, minmax(320px, 1fr)).
Repo Card spec: (see Section 2.6)
Selection UX:
- Clicking anywhere on a card (not links) while holding Shift selects a range
- "Select All" checkbox in filter bar header when ≥1 card is rendered
- Selection count badge floats on Bulk Action Bar
Bulk Action Bar spec: (see Section 2.6)
All actions live in lib/actions/bulk.ts. Use 'use server' directive.
// Pattern for all bulk actions
'use server'
import { auth } from "@/app/api/auth/[...nextauth]/route"
import { Octokit } from "octokit"
export async function bulkPrivatize(repoFullNames: string[]) {
const session = await auth()
const octokit = new Octokit({ auth: session!.accessToken })
const results = await Promise.allSettled(
repoFullNames.map(fullName => {
const [owner, repo] = fullName.split('/')
return octokit.rest.repos.update({ owner, repo, private: true })
})
)
return summarizeResults(results, repoFullNames)
}Implement all of these:
| Function | API call | Reversible? |
|---|---|---|
bulkPrivatize(names[]) |
PATCH /repos/{owner}/{repo} {private: true} |
Yes → bulkPublicize |
bulkPublicize(names[]) |
PATCH /repos/{owner}/{repo} {private: false} |
Yes → bulkPrivatize |
bulkArchive(names[]) |
PATCH /repos/{owner}/{repo} {archived: true} |
Yes → bulkUnarchive |
bulkUnarchive(names[]) |
PATCH /repos/{owner}/{repo} {archived: false} |
Yes |
bulkDelete(names[]) |
DELETE /repos/{owner}/{repo} |
No — require count confirm |
bulkAddTopics(names[], topics[]) |
PUT /repos/{owner}/{repo}/topics |
Yes |
bulkRemoveTopics(names[], topics[]) |
PUT /repos/{owner}/{repo}/topics |
Yes |
bulkUpdateDescription(updates[]) |
PATCH /repos/{owner}/{repo} {description} |
Yes |
bulkRename(renames[]) |
PATCH /repos/{owner}/{repo} {name} |
Yes |
Rate Limit Handling:
- GitHub REST: 5000 req/hour for authenticated users
- Detect
403withX-RateLimit-Remaining: 0 - On rate limit: pause, show "Rate limited — resuming in Xs" toast with live countdown, retry
- Use
Promise.allSettled— neverPromise.all— so partial failures don't abort everything - Return
{ succeeded: string[], failed: { name: string, error: string }[] }from every action
Delete Confirmation Modal:
Deleting 7 repositories
This action is permanent and cannot be undone.
These repos will be gone forever.
Type [7] to confirm:
[___________]
[Cancel] [Delete 7 repos — terracotta button, disabled until count matches]
Open a modal with a rename pattern editor:
Rename 5 repositories
Mode: [Add prefix ▾] [Add suffix] [Find & replace] [Custom per repo]
Prefix: [archive-_____________]
Preview:
my-old-project → archive-my-old-project
test-thing → archive-test-thing
unused-api → archive-unused-api
[Cancel] [Apply Rename]
Custom per-repo mode shows a table with editable name fields.
Open a panel from the Bulk Action Bar:
Manage Topics for 6 repos
Add topics: [+ react] [+ typescript] [+ nextjs] [Type to add...]
Remove topics (found across selected repos):
[✕ javascript] [✕ webpack] [✕ create-react-app]
[Cancel] [Apply]
A surfaced "view" in the Dashboard filter bar: "Dead Repos" filter preset.
Definition: pushed_at older than 6 months AND not archived.
When active, shows a banner:
🪦 23 repos haven't had activity in 6+ months.
Bulk archive them to clean up your profile.
[Select All Dead] [Archive Selected]
Page Header:
Starred Repositories
[N total stars] [X languages]
[Search stars...] [Language ▾] [Sort: Recently starred ▾]
Star Card: Same card design as repo card but lighter — shows original owner/repo, star date, language, description, and an "Unstar" hover button (amber star → grey).
Bulk Unstar:
- Same multi-select UX as dashboard
- Bulk Action Bar shows only: [Unstar Selected]
- No destructive confirm needed (re-starring is easy)
Grouping toggle: Flat list OR grouped by language (accordion sections).
"Forgotten Stars" preset filter: Starred >1 year ago, never visited (no way to detect visits, so just age-based). Banner: "You starred these 1+ year ago. Still relevant?"
API: GET /user/starred (paginated, 100 per page — fetch all via loop). Unstar: DELETE /user/starred/{owner}/{repo}.
GitHub lets you pin exactly 6 items (repos or gists) on your profile. This is only editable via GitHub GraphQL API.
Page Layout:
Profile Pins
Your profile shows these 6 pinned items.
Drag to reorder. Click × to remove. Click + to add from your repos.
[Pin 1 card] [Pin 2 card] [Pin 3 card]
[Pin 4 card] [Pin 5 card] [Pin 6 card — or Add +]
[Save Pins — primary button]
Pin Card: Draggable. Shows repo name, description, language, stars. Drag handle on left. Remove × on top-right.
Add Pin Modal: Searchable list of your non-pinned repos. Click to add (up to 6 total).
Drag & Drop: Use @dnd-kit/core and @dnd-kit/sortable. Smooth spring animation on reorder.
GraphQL mutations:
mutation UpdatePinnedItems($input: UpdateUserPinnedItemsInput!) {
updateUserPinnedItems(input: $input) {
pinnedItems {
nodes {
... on Repository {
id
name
description
}
}
}
}
}Fetch current pins:
query GetPinnedItems {
viewer {
pinnedItems(first: 6, types: [REPOSITORY]) {
nodes {
... on Repository {
id
name
description
stargazerCount
primaryLanguage { name color }
}
}
}
}
}The problem: GitHub has no unified view of all your open PRs and issues across all repos.
Page Layout:
Activity Feed
[Tab: Open PRs (N)] [Tab: Open Issues (N)] [Tab: Stale Issues (N)]
[Filter: All repos ▾] [Filter: All labels ▾] [Sort: Updated ▾]
Feed Item Card:
[repo-owner/repo-name] [PR #123 or Issue #456]
Title of the PR or Issue [2 days ago]
[Labels: bug enhancement] [Assignees: avatar avatar]
Clicking a feed item opens in a new tab to the actual GitHub URL.
Stale Issues Tab:
- Issues open for more than 30/60/90 days (configurable filter)
- Bulk action: Bulk Close Issues — posts a comment "Closing as stale via GitFit" then closes
- This is the only Stale-specific action. No auto-labeling (requires label management scope).
API:
GET /user/issues?filter=all&state=open&sort=updated&per_page=100
GET /user/pulls?state=open&sort=updated&per_page=100
Note: /user/issues returns issues from repos you own or are assigned to. Paginate fully.
Bulk Action Bar for issues:
[N selected] | [Close Issues] [Add Label ▾] [Assign ▾]
Close Issues action:
export async function bulkCloseIssues(issues: { owner: string, repo: string, number: number }[]) {
// POST /repos/{owner}/{repo}/issues/{issue_number}/comments
// body: "Closing as stale. Managed via [GitFit](https://gitfit.vercel.app)."
// PATCH /repos/{owner}/{repo}/issues/{issue_number} { state: "closed" }
}Add Label to Issues (bulk):
export async function bulkAddLabels(issues: ..., labels: string[]) {
// POST /repos/{owner}/{repo}/issues/{issue_number}/labels
}// hooks/useRepos.ts
import { useQuery } from '@tanstack/react-query'
export function useRepos() {
return useQuery({
queryKey: ['repos'],
queryFn: fetchAllRepos,
staleTime: 5 * 60 * 1000, // 5 min — repos don't change that fast
gcTime: 30 * 60 * 1000,
})
}GitHub returns max 100 repos per page. Fetch all:
export async function fetchAllRepos(accessToken: string) {
const octokit = new Octokit({ auth: accessToken })
const repos = await octokit.paginate(octokit.rest.repos.listForAuthenticatedUser, {
per_page: 100,
sort: 'updated',
direction: 'desc',
affiliation: 'owner' // owned only — not collaborator repos
})
return repos
}Same pattern for stars: octokit.paginate(octokit.rest.activity.listReposStarredByAuthenticatedUser, ...).
After a bulk action succeeds on the server, update the TanStack Query cache directly:
// After bulkArchive succeeds:
queryClient.setQueryData(['repos'], (old: Repo[]) =>
old.map(repo =>
archivedNames.includes(repo.full_name)
? { ...repo, archived: true }
: repo
)
)For delete: filter them out of cache immediately. For failed items: show error toast listing failed repo names.
All filtering happens client-side after the full repo list is fetched. This ensures instant filter UX with zero API calls.
export function filterRepos(repos: Repo[], filters: Filters): Repo[] {
return repos
.filter(r => {
if (filters.search) {
const q = filters.search.toLowerCase()
if (!r.name.toLowerCase().includes(q) &&
!(r.description?.toLowerCase().includes(q))) return false
}
if (filters.visibility === 'public' && r.private) return false
if (filters.visibility === 'private' && !r.private) return false
if (filters.status === 'archived' && !r.archived) return false
if (filters.status === 'active' && r.archived) return false
if (filters.language && r.language !== filters.language) return false
if (filters.preset === 'dead') {
const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
if (new Date(r.pushed_at) > sixMonthsAgo || r.archived) return false
}
return true
})
.sort((a, b) => {
switch (filters.sort) {
case 'name': return a.name.localeCompare(b.name)
case 'stars': return b.stargazers_count - a.stargazers_count
case 'size': return b.size - a.size
case 'created': return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
default: return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
}
})
}- Default: repo card shows no checkbox
- On card hover: checkbox fades in (opacity 0 → 1, 120ms)
- When ANY card is selected: ALL cards show checkboxes permanently until selection is cleared
- Clicking the card body (not the repo name link) toggles selection
- Shift+click selects a range
.bulk-bar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(120%);
transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1); /* spring overshoot */
}
.bulk-bar.visible {
transform: translateX(-50%) translateY(0);
}- Archive / Privatize / Publicize / Tag / Rename: Immediate, no confirm. Toast with undo (3s).
- Delete: Modal confirm (type count). No undo.
- Bulk unstar: Immediate, no confirm. Toast only.
- Bulk close issues: Confirm dialog. No undo.
[✓ Archived 7 repos] [Undo →]
[━━━━━━━━━━░░░░░░░░░░] (3 second countdown bar animates left to right)
Undo calls the reverse action (e.g. bulkUnarchive with the same names). After 3s, toast auto-dismisses.
- Initial repo load: Skeleton cards (6 cards, same dimensions as real cards, animated shimmer)
- Shimmer uses a CSS
@keyframesmoving gradient:--bg-surface→--bg-elevated→--bg-surface - Bulk action in progress: Action bar buttons show a subtle spinner, cards being affected get a pulsing opacity (0.5 → 1 loop)
- Never block the whole page — always partial loading states
Each page needs a considered empty state, not a blank screen:
No repos match filters:
🔍
No repos match your filters.
Try broadening your search or clearing filters.
[Clear filters]
No stars:
⭐
No starred repositories yet.
Head to GitHub and star some repos.
No open issues:
✓
All clear.
No open issues across your repositories.
| Key | Action |
|---|---|
/ |
Focus search |
Escape |
Clear selection / close modal |
A |
Select all (when no input focused) |
Shift+A |
Deselect all |
Show a keyboard shortcuts tooltip (bottom-left corner, always visible, small ? icon).
1. Header fades in (opacity 0→1, y: -8→0, duration: 300ms)
2. Filter bar fades in (delay: 80ms)
3. Repo cards stagger in (delay: 40ms per card, max 8 cards animated, rest instant)
Each card: opacity 0→1, y: 12→0, duration: 250ms, ease-out
Checkbox appears: opacity 0→1, scale 0.8→1, 120ms
Card border: color transition 200ms
Card box-shadow: 200ms ease
Spring bounce on appear (cubic-bezier overshoot, see 10.2).
Use @dnd-kit/sortable with CSS.Transform.toString(transform) and transition: transform 200ms ease. Dragging card: scale 1.02, shadow increase, z-index elevation.
Mobile: < 640px — single column, hamburger sidebar
Tablet: 640–1024px — 2-column grid, collapsible sidebar
Desktop: > 1024px — 3-column grid (or 2 for wider cards), full sidebar
Wide: > 1280px — max-width container centers content
Bulk Action Bar on mobile: full-width, sticks to bottom of viewport. Actions scroll horizontally.
Filter Bar on mobile: horizontal scrolling strip. No line breaks.
Simple preferences, stored in localStorage (no server state needed):
Appearance
Theme: [Warm Dark ●] [Light] (light mode flips palette to --bg-canvas: #faf7f2)
Dashboard
Repos per load: [50] [100] [All]
Default sort: [Last updated ▾]
Default view: [Grid] [List]
Feed
Stale threshold: [30 days] [60 days] [90 days]
Keyboard shortcuts: [View all]
Danger Zone
Disconnect GitHub account — [Sign out]
| Scenario | Behavior |
|---|---|
| Token expired / revoked | Redirect to sign-in with "Session expired" message |
| GitHub API rate limit hit | Pause operations, show live countdown toast |
| Partial bulk failure | Show success count + list failed repos in error toast |
| Repo has open PRs (delete) | GitHub API will still reject — surface error: "Cannot delete: has open PRs" |
| Archived repo can't be deleted | Surface clearly: "Unarchive before deleting" |
| No repos | Empty state with a link to github.com/new |
| Over 6 pins | UI prevents adding a 7th pin (button greys out) |
| Network offline | Toast: "You're offline. Changes will resume when connection returns." |
Follow this exact order. Each item = one commit (or a small logical group).
chore: init Next.js 14 app with TypeScript, Tailwind, ESLint
chore: configure Tailwind with design tokens and CSS variables
chore: add Fraunces font + JetBrains Mono via Google Fonts
chore: add .env.example and .gitignore
chore: create project folder structure (empty files with comments)
feat(auth): add NextAuth v5 with GitHub provider and required OAuth scopes
feat(auth): persist accessToken in session via jwt + session callbacks
feat(auth): add auth middleware to protect /dashboard, /stars, /feed, /pins
feat(auth): add sign-in redirect from / when unauthenticated
style(landing): build landing page hero with GitFit logotype
style(landing): add feature grid to landing page
style(landing): add landing page footer with privacy note
style(layout): build authenticated sidebar with navigation items
style(layout): build app header with user menu and avatar
style(layout): add mobile hamburger + slide-over sidebar
feat(github): add Octokit REST client factory using session token
feat(github): add fetchAllRepos with pagination (octokit.paginate)
feat(github): add GitFitRepo type mapping from GitHub API response
feat(dashboard): add TanStack Query provider and useRepos hook
feat(dashboard): build repo grid with RepoCard skeleton loading state
style(dashboard): build RepoCard component (name, description, badges, meta row)
feat(dashboard): add FilterBar (search, visibility, status, language, sort)
feat(dashboard): implement client-side filterRepos + sortRepos logic
style(dashboard): add page header with repo count stat pills
feat(selection): implement useSelection hook (toggle, shift-range, select-all)
style(selection): add checkbox reveal on card hover + selection state styling
feat(selection): build BulkActionBar with spring appear/dismiss animation
feat(bulk): implement bulkArchive server action
feat(bulk): implement bulkUnarchive server action
feat(bulk): add optimistic update + undo toast for archive/unarchive
feat(bulk): implement bulkPrivatize server action
feat(bulk): implement bulkPublicize server action
feat(bulk): add optimistic update + undo toast for privatize/publicize
feat(bulk): implement bulkAddTopics + bulkRemoveTopics server actions
feat(bulk): build BulkTopicEditor modal/panel
feat(bulk): implement bulkUpdateDescription server action
feat(bulk): implement bulkRename server action with prefix/suffix/find-replace
feat(bulk): build BulkRenameModal with live preview
feat(bulk): implement bulkDelete server action
feat(bulk): build BulkDeleteConfirmModal (count-typed confirmation)
feat(bulk): add rate limit detection and retry-after countdown toast
feat(dashboard): add Dead Repos preset filter (pushed >6 months, not archived)
style(dashboard): add Dead Repos banner with Select All Dead + Archive CTA
feat(stars): add fetchAllStars with pagination
feat(stars): build /stars page with StarCard component
feat(stars): add search + language filter for stars
feat(stars): add Forgotten Stars preset filter (starred >1 year ago)
feat(stars): implement unstar and bulkUnstar server actions
feat(stars): add grouped-by-language toggle view
feat(github): add GitHub GraphQL client with token
feat(github): add GetPinnedItems GraphQL query
feat(pins): build /pins page layout
feat(pins): install @dnd-kit/core @dnd-kit/sortable
feat(pins): build draggable PinCard with @dnd-kit/sortable
feat(pins): build AddPin modal with searchable repo list
feat(pins): implement updatePins GraphQL mutation server action
style(pins): polish drag animation (spring transform, shadow elevation)
feat(github): add fetchUserIssues + fetchUserPRs with pagination
feat(feed): build /feed page with tabbed layout (PRs / Issues / Stale)
feat(feed): build FeedItem card component
feat(feed): add filter by repo + sort controls
feat(feed): implement stale threshold filter (30/60/90 days)
feat(feed): implement bulkCloseIssues server action (comment + close)
feat(feed): implement bulkAddLabels server action
feat(feed): add bulk selection + BulkActionBar on feed page
feat(settings): build /settings page
feat(settings): add theme + dashboard + feed preferences to localStorage
feat(keyboard): add / shortcut to focus search
feat(keyboard): add Escape to clear selection/close modal
feat(keyboard): add A/Shift+A to select all / deselect all
feat(keyboard): add keyboard shortcuts tooltip (? icon, bottom-left)
style(animations): add staggered page-load animations to dashboard
style(animations): add shimmer skeleton loading state
style(empty-states): add empty states for all pages
style(responsive): audit and fix mobile layouts (320px minimum)
chore: audit OAuth scopes — confirm delete_repo scope works for bulk delete
chore: add error boundary for API failures
chore: final accessibility pass (focus rings, aria-labels, screen reader text)
docs: write README.md with setup instructions and feature list
chore: configure Vercel deployment, add all env vars to Vercel dashboard
# GitFit — Your GitHub, finally under control.
GitFit is a better GitHub repository manager. Authenticate once with GitHub OAuth
and get bulk operations, smart filters, a stars manager, pin editor, and
cross-repo issue/PR feed — all in one warm, fast dashboard.
## Features
- **Bulk Actions** — Archive, privatize, delete, tag, rename N repos in one click
- **Smart Filters** — Dead repos, by language, by visibility, by last activity
- **Stars Manager** — Browse, search, bulk unstar your forgotten starred repos
- **Pin Editor** — Drag-and-drop pin reordering with live GitHub profile sync
- **Cross-repo Feed** — All open PRs and issues across all your repos, unified
- **Stale Issues** — Find and bulk-close issues that have been open for months
## Privacy
GitFit stores nothing. Zero databases. Your GitHub OAuth token lives in your
browser session only. All reads and writes go directly through GitHub's official API.
## Development
git clone https://github.com/HarshalPatel1972/GitFit.git
cd GitFit
cp .env.example .env.local
# Fill in GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, NEXTAUTH_SECRET
npm install
npm run dev
## Stack
Next.js 14 · NextAuth v5 · Octokit · TanStack Query · Tailwind CSS · Vercel- Org support — manage repos for GitHub orgs you admin
- Export — download repo list as CSV / JSON
- Insights — language distribution pie, activity heatmap across repos
- GitHub Actions feed — see failing/passing workflows across repos
- Collaborator manager — see who has access to what, bulk revoke
- Webhook manager — list and delete stale webhooks across repos
- Deploy badges — show Vercel/Netlify status badges inline on repo cards
GitFit FullBuildPrompt.md · All Phases · Built for HarshalPatel1972