A modern React SPA for exploring, searching, bookmarking, and comparing GitHub repositories. Built with React 19, TypeScript, Vite, Tailwind CSS 4, and shadcn/ui.
- Repository Search — Search GitHub repos with filters for language, star count, sort order, and creation date. Debounced input with keyboard shortcut (
/to focus). - Repository Details — View full repo info including stats, topics, dates, and an estimated star history chart.
- Bookmarks — Save repos to a persistent bookmark list (localStorage). Filter, sort, and export as JSON.
- Compare — Select up to 4 repos and compare them side-by-side with metrics and overlaid star history charts.
- Authentication — Sign in with a GitHub Personal Access Token to increase the API rate limit from 60 to 5,000 requests/hour.
- Dark/Light Theme — Toggle between themes with OS preference detection and localStorage persistence.
- Node.js 18 or later
- npm (comes with Node.js)
No API keys, environment variables, or external services are required. The app uses the public GitHub API out of the box.
git clone <repo-url>
cd github-explorer
npm installnpm run devOpen http://localhost:5173 in your browser. Vite provides hot module replacement, so changes to source files reflect instantly without a full page reload.
The app works immediately without authentication, but the public GitHub API is limited to 60 requests per hour. To get 5,000 requests per hour:
- Go to GitHub Settings > Tokens and generate a classic Personal Access Token (no scopes needed).
- Click Sign in in the app header and paste the token.
The token is stored in your browser's localStorage and never sent anywhere except the GitHub API. Rate limit status is shown in the header when authenticated.
| Script | Command | Description |
|---|---|---|
npm run dev |
vite |
Start dev server with HMR at localhost:5173 |
npm run build |
tsc -b && vite build |
Type-check and build for production into dist/ |
npm run preview |
vite preview |
Serve the production build locally |
npm run lint |
eslint . |
Check for lint issues |
npm run lint:fix |
eslint . --fix |
Auto-fix lint issues |
npm run type-check |
tsc --noEmit |
Type-check without emitting files |
npm run build
npm run previewThe first command type-checks and bundles everything into dist/. The second serves it locally at http://localhost:4173 so you can verify the build before deploying. The output is a static site — deploy dist/ to any static hosting provider (Vercel, Netlify, GitHub Pages, etc.).
src/
├── main.tsx # Entry point
├── App.tsx # Root component (routing, providers)
├── index.css # Global styles & CSS variables
│
├── pages/
│ ├── SearchPage.tsx # Search interface with filters
│ ├── RepoDetailPage.tsx # Single repo view with star chart
│ ├── BookmarksPage.tsx # Saved repos list
│ ├── ComparePage.tsx # Side-by-side comparison
│ └── NotFoundPage.tsx # 404
│
├── components/
│ ├── layout/
│ │ ├── Layout.tsx # App shell (header + footer)
│ │ └── Header.tsx # Navigation, auth, theme toggle
│ ├── repo/
│ │ └── RepoCard.tsx # Repo card for search results
│ ├── compare/
│ │ └── CompareBar.tsx # Floating bar for selected repos
│ ├── charts/
│ │ └── StarHistoryChart.tsx # Star growth area chart
│ └── ui/ # shadcn/ui primitives
│
├── hooks/
│ ├── useSearch.ts # React Query hook for search
│ └── useTheme.tsx # Theme context provider
│
├── store/
│ ├── authStore.ts # Auth state (token, user, rate limit)
│ ├── bookmarkStore.ts # Bookmarks (persisted to localStorage)
│ └── compareStore.ts # Compare queue (max 4 repos)
│
├── lib/
│ ├── api.ts # GitHub search API + language data
│ ├── github.ts # GitHubClient (auth, fetch, rate limits)
│ ├── starHistory.ts # Star history fetcher (sampled pages)
│ └── utils.ts # cn() utility
│
└── types/
└── github.ts # TypeScript interfaces
React Router v7 with lazy-loaded pages via React.lazy() + Suspense:
| Route | Page | Description |
|---|---|---|
/ |
SearchPage | Search and filter repositories |
/repo/:owner/:name |
RepoDetailPage | Repository details and star history |
/bookmarks |
BookmarksPage | Saved repositories |
/compare |
ComparePage | Side-by-side comparison |
* |
NotFoundPage | 404 fallback |
Three layers of state:
| Layer | Tool | Purpose |
|---|---|---|
| Global | Zustand | Auth, bookmarks, compare queue |
| Server | React Query | API data with caching (5-min stale time) |
| Local | React hooks | UI state (filters, dialogs, inputs) |
Bookmarks are persisted to localStorage via Zustand's persist middleware.
The app uses the GitHub REST API (api.github.com):
GET /search/repositories— Search with query filtersGET /repos/{owner}/{repo}— Fetch repo detailsGET /repos/{owner}/{repo}/stargazers— Sampled pagination for star history estimationGET /user— Validate token and fetch user profile
Rate limit headers are parsed from every response and surfaced in the UI. On 401, the token is auto-cleared. On 403 (rate limit exceeded), a user-friendly error with reset time is shown.
GitHub doesn't expose historical star counts directly. The app estimates star growth by sampling ~8 pages from the stargazers endpoint using logarithmic distribution and interpolating the timeline.
| Category | Technology |
|---|---|
| Framework | React 19 |
| Language | TypeScript 5.9 |
| Build | Vite 8 |
| Styling | Tailwind CSS 4 |
| Components | shadcn/ui (Radix UI) |
| State | Zustand |
| Server State | TanStack React Query |
| Routing | React Router v7 |
| Charts | Recharts |
| Icons | Lucide React |
| Theming | next-themes |
| Notifications | Sonner |
| Dates | date-fns |
- Fonts: Fira Code (headings/mono) + Fira Sans (body)
- Colors: Blue primary (#2563EB), orange accent (#F97316), slate neutrals
- Dark mode: Default, OLED-optimized with full CSS variable system
- Responsive: Mobile-first with 1/2/3 column grid breakpoints
- Accessibility: WCAG contrast ratios, keyboard navigation, focus rings, semantic HTML
Full design system specs are in design-system/github-stars-explorer/MASTER.md.
Private project.