Skip to content
Merged
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
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,28 @@ npm run lint # ESLint v9

Always run `npm run lint` after making changes. No typecheck script is configured; verify types via `npm run build` or `npx tsc --noEmit`.

Do not commit changes unless explicitly asked to.

All UI text is in English.

## Search API

GitHub Search API results are fetched one page at a time (100 per page, max 1000
results). The initial `searchUsers()` / `searchRepositories()` calls fetch only
the first page and return pagination metadata (`total_pages`, `next_page`,
`prev_page`). Use `githubApi.fetchSearchPage(endpoint, query, page, extraParams?)`
to load additional pages on demand. The response type is `GitHubSearchResponse<T>`,
exported from `src/lib/github.ts`.

```typescript
// First page
const res = await githubApi.searchUsers("torvalds");
// res.items, res.total_pages, res.next_page

// Subsequent page
const page2 = await githubApi.fetchSearchPage("/search/users", "torvalds", 2);
```

## Node.js

- **Required:** `>=22.0.0` (enforced via `package.json` `engines`)
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# GitHub Explorer

High-performance dashboard for exploring GitHub profiles and repositories.
High-performance dashboard for exploring GitHub users, repositories, and search results.

**v0.2.0**
**v0.3.0**

## Features

- **Smart search** — auto-detects whether input is a user (`torvalds`) or repository (`facebook/react`)
- **Combined search** — single query searches both users and repositories simultaneously with paginated results
- **Search results** — dedicated results view with user cards and repository cards, each independently paginated
- **User dashboard** — full profile, aggregated stats (stars, forks), language distribution chart, repository list with search, language filter, sorting, and pagination
- **Repository dashboard** — metadata, rendered README, commit timeline, contributors, language breakdown with percent bar
- **Light / Dark theme** — toggles via navbar button, follows system preference by default, persists choice in a cookie (365 days)
Expand Down Expand Up @@ -65,19 +66,21 @@ docker run -p 3000:3000 github-explorer
```
src/
├── app/
│ ├── api/github/[...path]/route.ts # GitHub API proxy (rate-limit, path whitelist)
│ ├── layout.tsx # root layout with Geist fonts, theme-color, ThemeProvider
│ ├── page.tsx # main page with view switching + footer
│ └── globals.css # global styles, GitHub Primer palette, light-dark() variables
├── components/
│ ├── LayoutHeader.tsx # sticky header with nav, theme toggle, logo → project repo
│ ├── LayoutHeader.tsx # sticky header with nav, theme toggle, logo → search view
│ ├── ThemeToggle.tsx # light/dark toggle (Sun/Moon)
│ ├── ViewSearch.tsx # initial search screen
│ ├── ViewSearchResults.tsx # combined user/repo results with pagination
│ ├── ViewUser.tsx # user profile dashboard
│ ├── ViewRepo.tsx # repository dashboard
│ ├── DashboardSkeletons.tsx
│ └── ui/ # base components (shadcn-style)
├── lib/
│ ├── github.ts # GitHub REST API client
│ ├── github.ts # GitHub REST API client with paginated search
│ ├── theme.tsx # ThemeProvider, useTheme, cookie persistence
│ └── utils.ts # cn() utility
└── store/
Expand Down
8 changes: 6 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-explorer-dashboard",
"version": "0.2.3",
"version": "0.3.0",
"private": true,
"engines": {
"node": ">=22.0.0",
Expand Down
5 changes: 5 additions & 0 deletions public/avatar-placeholder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/app/api/github/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const ALLOWED_PATHS = [
/^\/repos\/[^/]+\/[^/]+\/commits$/,
/^\/repos\/[^/]+\/[^/]+\/contributors$/,
/^\/repos\/[^/]+\/[^/]+\/languages$/,
/^\/search\/users$/,
/^\/search\/repositories$/,
] as const;

function isAllowedPath(pathname: string) {
Expand Down
19 changes: 6 additions & 13 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import React from "react";
import { useAppStore } from "@/store/useAppStore";
import { LayoutHeader } from "@/components/LayoutHeader";
import { ViewSearch } from "@/components/ViewSearch";
import { ViewSearchResults } from "@/components/ViewSearchResults";
import { ViewUser } from "@/components/ViewUser";
import { ViewRepo } from "@/components/ViewRepo";
import {
UserDashboardSkeleton,
RepoDashboardSkeleton,
} from "@/components/DashboardSkeletons";
import { GithubIcon } from "@/components/ui/icons";
import { AnimatePresence, motion } from "framer-motion";

export default function Home() {
const { view, loading, loadingType } = useAppStore();
Expand All @@ -26,6 +26,8 @@ export default function Home() {
}

switch (view) {
case "results":
return <ViewSearchResults key="results-view" />;
case "user":
return <ViewUser key="user-view" />;
case "repo":
Expand All @@ -47,18 +49,9 @@ export default function Home() {
<div className="absolute top-[-10%] left-[20%] w-[300px] h-[300px] rounded-full bg-violet-600/10 blur-[120px] pointer-events-none select-none" />
<div className="absolute bottom-[20%] right-[10%] w-[350px] h-[350px] rounded-full bg-emerald-600/5 blur-[150px] pointer-events-none select-none" />

<AnimatePresence mode="wait">
<motion.div
key={loading ? `loading-${loadingType}` : `view-${view}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
className="flex-1 flex flex-col w-full h-full"
>
{renderContent()}
</motion.div>
</AnimatePresence>
<div className="flex-1 flex flex-col w-full h-full">
{renderContent()}
</div>
</main>

{/* Modern Minimalist Footer */}
Expand Down
19 changes: 2 additions & 17 deletions src/components/LayoutHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,7 @@ import { ThemeToggle } from "@/components/ThemeToggle";
import { motion, AnimatePresence } from "framer-motion";

export const LayoutHeader = () => {
const { view, navigationStack, popNavigation, search } = useAppStore();

const goToProjectRepo = () => {
search("ziguifrido/github-explorer");
};

const resetToSearch = () => {
useAppStore.setState({
navigationStack: [{ view: "search" }],
view: "search",
username: "",
repoOwner: "",
repoName: "",
error: null,
});
};
const { view, navigationStack, popNavigation, resetToSearch } = useAppStore();

// Check if we came from a User dashboard to the Repository dashboard
const hasUserInStack = React.useMemo(() => {
Expand All @@ -40,7 +25,7 @@ export const LayoutHeader = () => {
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
{/* Logo / Title */}
<div
onClick={goToProjectRepo}
onClick={resetToSearch}
className="flex items-center gap-2 cursor-pointer group"
>
<div className="w-8 h-8 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:border-border transition-colors">
Expand Down
6 changes: 4 additions & 2 deletions src/components/ViewSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ViewSearch = () => {
{ label: 'Torvalds', value: 'torvalds' },
{ label: 'React Project', value: 'facebook/react' },
{ label: 'Next.js Project', value: 'vercel/next.js' },
{ label: 'Vite Developer', value: 'yyx990803' },
{ label: 'Machine Learning', value: 'machine learning' },
];

return (
Expand Down Expand Up @@ -96,7 +96,7 @@ export const ViewSearch = () => {
}}
onKeyDown={handleKeyDown}
disabled={loading}
placeholder="Enter a username (e.g. 'torvalds') or repository (e.g. 'facebook/react')..."
placeholder="Search users, repositories, or type 'user/repo'..."
className="w-full bg-transparent resize-none outline-none border-none py-2 px-3 text-sm md:text-base text-foreground placeholder-tertiary min-h-[44px] max-h-[180px] scrollbar-none font-sans"
/>

Expand Down Expand Up @@ -213,6 +213,8 @@ export const ViewSearch = () => {
<div className="w-6 h-6 rounded-md bg-muted/60 border border-border flex items-center justify-center shrink-0">
{item.type === 'repo' ? (
<Compass className="w-3.5 h-3.5 text-tertiary group-hover:text-foreground transition-colors" />
) : item.type === 'search' ? (
<History className="w-3.5 h-3.5 text-tertiary group-hover:text-foreground transition-colors" />
) : (
<GithubIcon className="w-3.5 h-3.5 text-tertiary group-hover:text-foreground transition-colors" />
)}
Expand Down
Loading