Skip to content

Commit 28738ee

Browse files
committedFeb 25, 2025
add: initial files
0 parents  commit 28738ee

16 files changed

+5876
-0
lines changed
 

‎.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# GitHub Personal Access Token for accessing private repositories
2+
# Create one at: https://github.com/settings/tokens
3+
# Required scopes: repo (for private repos), read:packages (optional)
4+
VITE_GITHUB_TOKEN=your_github_token_here

‎.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

‎README.md

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# GitHub Repository Content Viewer (React)
2+
3+
A modern React application demonstrating how to fetch and display GitHub repository contents using the GitHub REST API. This application supports both public and private repositories.
4+
5+
## Features
6+
7+
- View public repository contents without authentication
8+
- Access private repositories using a GitHub Personal Access Token
9+
- Navigate through repository directories
10+
- View file contents
11+
- Modern React components with hooks
12+
- Clean, responsive UI
13+
14+
## Setup and Installation
15+
16+
1. Clone this repository
17+
2. Install dependencies:
18+
19+
```bash
20+
npm install
21+
```
22+
23+
3. Create a `.env` file in the root directory and add your GitHub token (optional, for private repos):
24+
25+
```bash
26+
VITE_GITHUB_TOKEN=your_personal_access_token
27+
```
28+
29+
## Usage
30+
31+
1. Start the development server:
32+
33+
```bash
34+
npm run dev
35+
```
36+
37+
2. Open your browser and navigate to `http://localhost:5173`
38+
3. Enter a repository in the format "owner/repository"
39+
4. For private repositories, ensure you've added your GitHub token in the `.env` file
40+
41+
## GitHub Token Setup (for private repositories)
42+
43+
1. Go to GitHub Settings > Developer settings > Personal access tokens
44+
2. Generate a new token with the following scopes:
45+
- `repo` (Full control of private repositories)
46+
- `read:packages` (Optional, for accessing package contents)
47+
3. Copy the token and add it to your `.env` file
48+
49+
## API Reference
50+
51+
This project uses the GitHub REST API v2022-11-28. For more information, see:
52+
53+
- [GitHub Contents API Documentation](https://docs.github.com/en/rest/repos/contents)
54+
55+
## Technical Details
56+
57+
- Built with React + Vite
58+
- Uses Octokit REST client for GitHub API
59+
- Modern ES6+ syntax
60+
- Environment variables for secure token management
61+
62+
## Limitations
63+
64+
- Rate limiting applies (60 requests/hour for unauthenticated requests, 5000 requests/hour with authentication)
65+
- File size limitations apply as per GitHub API
66+
- Binary files are not displayed (only text-based files)
67+
68+
## License
69+
70+
MIT License - Feel free to use and modify as needed.

‎eslint.config.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import react from 'eslint-plugin-react'
4+
import reactHooks from 'eslint-plugin-react-hooks'
5+
import reactRefresh from 'eslint-plugin-react-refresh'
6+
7+
export default [
8+
{ ignores: ['dist'] },
9+
{
10+
files: ['**/*.{js,jsx}'],
11+
languageOptions: {
12+
ecmaVersion: 2020,
13+
globals: globals.browser,
14+
parserOptions: {
15+
ecmaVersion: 'latest',
16+
ecmaFeatures: { jsx: true },
17+
sourceType: 'module',
18+
},
19+
},
20+
settings: { react: { version: '18.3' } },
21+
plugins: {
22+
react,
23+
'react-hooks': reactHooks,
24+
'react-refresh': reactRefresh,
25+
},
26+
rules: {
27+
...js.configs.recommended.rules,
28+
...react.configs.recommended.rules,
29+
...react.configs['jsx-runtime'].rules,
30+
...reactHooks.configs.recommended.rules,
31+
'react/jsx-no-target-blank': 'off',
32+
'react-refresh/only-export-components': [
33+
'warn',
34+
{ allowConstantExport: true },
35+
],
36+
},
37+
},
38+
]

‎index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.jsx"></script>
12+
</body>
13+
</html>

‎package-lock.json

+4,876
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "getrepo",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@octokit/rest": "^21.1.1",
14+
"axios": "^1.7.9",
15+
"react": "^19.0.0",
16+
"react-dom": "^19.0.0",
17+
"react-icons": "^5.5.0"
18+
},
19+
"devDependencies": {
20+
"@eslint/js": "^9.21.0",
21+
"@types/react": "^19.0.10",
22+
"@types/react-dom": "^19.0.4",
23+
"@vitejs/plugin-react": "^4.3.4",
24+
"eslint": "^9.21.0",
25+
"eslint-plugin-react": "^7.37.4",
26+
"eslint-plugin-react-hooks": "^5.0.0",
27+
"eslint-plugin-react-refresh": "^0.4.19",
28+
"globals": "^15.15.0",
29+
"vite": "^6.2.0"
30+
}
31+
}

‎public/vite.svg

+1
Loading

‎src/App.css

+326
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/* Base styles */
2+
:root {
3+
--primary-color: #000000;
4+
--primary-hover: #333333;
5+
--background-color: #ffffff;
6+
--text-color: #000000;
7+
--border-color: #dddddd;
8+
--error-color: #ff0000;
9+
--success-color: #008000;
10+
--input-background: #f8f8f8;
11+
}
12+
13+
* {
14+
margin: 0;
15+
padding: 0;
16+
box-sizing: border-box;
17+
}
18+
19+
body {
20+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
21+
background-color: var(--background-color);
22+
color: var(--text-color);
23+
line-height: 1.6;
24+
display: flex;
25+
justify-content: center;
26+
min-height: 100vh;
27+
}
28+
29+
#root {
30+
width: 100%;
31+
display: flex;
32+
justify-content: center;
33+
}
34+
35+
.app {
36+
width: 100%;
37+
max-width: 1400px;
38+
margin: 0 auto;
39+
padding: 2rem;
40+
}
41+
42+
/* Header styles */
43+
header {
44+
margin-bottom: 3rem;
45+
text-align: center;
46+
}
47+
48+
h1 {
49+
font-size: 1.5rem;
50+
margin-bottom: 1rem;
51+
color: var(--text-color);
52+
font-weight: 400;
53+
}
54+
55+
/* Token input styles */
56+
.token-input {
57+
margin: 1rem 0;
58+
display: flex;
59+
gap: 0.5rem;
60+
justify-content: center;
61+
align-items: center;
62+
flex-wrap: wrap;
63+
}
64+
65+
.token-input input {
66+
padding: 0.5rem;
67+
border: 1px solid var(--border-color);
68+
width: 100%;
69+
max-width: 400px;
70+
font-family: monospace;
71+
background-color: var(--input-background);
72+
color: var(--text-color);
73+
}
74+
75+
.token-input small {
76+
color: #666666;
77+
font-size: 0.875rem;
78+
width: 100%;
79+
text-align: center;
80+
}
81+
82+
/* Repository viewer styles */
83+
.repo-viewer {
84+
display: grid;
85+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
86+
gap: 2rem;
87+
align-items: start;
88+
}
89+
90+
.viewer-left,
91+
.viewer-right {
92+
background-color: white;
93+
border: 1px solid var(--border-color);
94+
padding: 1.5rem;
95+
}
96+
97+
.repo-form {
98+
grid-column: 1 / -1;
99+
display: flex;
100+
gap: 0.5rem;
101+
margin-bottom: 1rem;
102+
justify-content: center;
103+
}
104+
105+
.repo-form input {
106+
width: 100%;
107+
max-width: 400px;
108+
padding: 0.5rem;
109+
border: 1px solid var(--border-color);
110+
font-family: monospace;
111+
background-color: var(--input-background);
112+
color: var(--text-color);
113+
}
114+
115+
/* Button styles */
116+
button {
117+
padding: 0.5rem 1rem;
118+
background-color: var(--primary-color);
119+
color: white;
120+
border: none;
121+
cursor: pointer;
122+
font-size: 0.875rem;
123+
transition: background-color 0.2s;
124+
}
125+
126+
button:hover {
127+
background-color: var(--primary-hover);
128+
}
129+
130+
/* API Documentation styles */
131+
.api-docs {
132+
margin-bottom: 1rem;
133+
}
134+
135+
.api-docs h2 {
136+
font-size: 1.2rem;
137+
margin-bottom: 1rem;
138+
font-weight: 400;
139+
}
140+
141+
.api-section {
142+
margin-bottom: 2rem;
143+
}
144+
145+
.api-section h3 {
146+
font-size: 1rem;
147+
margin-bottom: 0.5rem;
148+
font-weight: 400;
149+
}
150+
151+
.code-block {
152+
background-color: var(--input-background);
153+
padding: 1rem;
154+
margin: 0.5rem 0;
155+
font-family: monospace;
156+
font-size: 0.875rem;
157+
overflow-x: auto;
158+
border: 1px solid var(--border-color);
159+
}
160+
161+
/* Breadcrumb styles */
162+
.breadcrumb {
163+
display: flex;
164+
align-items: center;
165+
gap: 0.5rem;
166+
margin-bottom: 1rem;
167+
padding: 0.5rem;
168+
background-color: var(--input-background);
169+
flex-wrap: wrap;
170+
}
171+
172+
.breadcrumb-button {
173+
background: none;
174+
color: var(--text-color);
175+
padding: 0.25rem 0.5rem;
176+
}
177+
178+
.breadcrumb-button:hover {
179+
background-color: #eeeeee;
180+
}
181+
182+
.breadcrumb-separator {
183+
color: #666666;
184+
font-size: 0.75rem;
185+
}
186+
187+
/* Contents styles */
188+
.contents {
189+
border: 1px solid var(--border-color);
190+
}
191+
192+
.content-item {
193+
display: flex;
194+
align-items: center;
195+
gap: 0.75rem;
196+
padding: 0.75rem 1rem;
197+
border-bottom: 1px solid var(--border-color);
198+
cursor: pointer;
199+
}
200+
201+
.content-item:hover {
202+
background-color: var(--input-background);
203+
}
204+
205+
.item-name {
206+
flex: 1;
207+
font-family: monospace;
208+
}
209+
210+
.item-size {
211+
color: #666666;
212+
font-size: 0.875rem;
213+
font-family: monospace;
214+
}
215+
216+
/* File viewer styles */
217+
.file-viewer {
218+
margin-top: 1rem;
219+
border: 1px solid var(--border-color);
220+
}
221+
222+
.file-header {
223+
display: flex;
224+
justify-content: space-between;
225+
align-items: center;
226+
padding: 0.75rem 1rem;
227+
background-color: var(--input-background);
228+
border-bottom: 1px solid var(--border-color);
229+
}
230+
231+
.file-content {
232+
padding: 1rem;
233+
overflow-x: auto;
234+
background-color: white;
235+
font-family: monospace;
236+
font-size: 0.875rem;
237+
line-height: 1.5;
238+
}
239+
240+
/* Token setup guide styles */
241+
.token-guide {
242+
margin: 2rem 0;
243+
padding: 1rem;
244+
border: 1px solid var(--border-color);
245+
grid-column: 1 / -1;
246+
}
247+
248+
.token-guide h2 {
249+
font-size: 1.2rem;
250+
margin-bottom: 1rem;
251+
font-weight: 400;
252+
}
253+
254+
.token-guide ol {
255+
padding-left: 1.5rem;
256+
}
257+
258+
.token-guide li {
259+
margin-bottom: 0.5rem;
260+
}
261+
262+
/* Utility styles */
263+
.error {
264+
color: var(--error-color);
265+
padding: 0.75rem;
266+
margin-bottom: 1rem;
267+
border: 1px solid var(--error-color);
268+
text-align: center;
269+
grid-column: 1 / -1;
270+
}
271+
272+
.loading {
273+
text-align: center;
274+
padding: 2rem;
275+
color: #666666;
276+
grid-column: 1 / -1;
277+
}
278+
279+
/* Footer styles */
280+
footer {
281+
margin-top: 2rem;
282+
text-align: center;
283+
color: #666666;
284+
font-size: 0.875rem;
285+
}
286+
287+
footer a {
288+
color: var(--text-color);
289+
text-decoration: none;
290+
border-bottom: 1px solid var(--text-color);
291+
}
292+
293+
footer a:hover {
294+
border-bottom-width: 2px;
295+
}
296+
297+
/* Responsive design */
298+
@media (max-width: 1024px) {
299+
.repo-viewer {
300+
grid-template-columns: 1fr;
301+
}
302+
303+
.viewer-left,
304+
.viewer-right {
305+
grid-column: 1 / -1;
306+
}
307+
}
308+
309+
@media (max-width: 640px) {
310+
.app {
311+
padding: 1rem;
312+
}
313+
314+
.repo-form {
315+
flex-direction: column;
316+
align-items: center;
317+
}
318+
319+
.token-input {
320+
flex-direction: column;
321+
}
322+
323+
.breadcrumb {
324+
justify-content: center;
325+
}
326+
}

‎src/App.jsx

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useState } from "react"
2+
import { Octokit } from "@octokit/rest"
3+
import RepoViewer from "./components/RepoViewer"
4+
import TokenInput from "./components/TokenInput"
5+
import "./App.css"
6+
7+
// Initialize Octokit with the token from environment variables (if available)
8+
const createOctokit = token => {
9+
return new Octokit({
10+
auth: token,
11+
userAgent: "GitHub-Content-Viewer-React v1.0",
12+
})
13+
}
14+
15+
function App() {
16+
// State for GitHub token
17+
const [token, setToken] = useState(import.meta.env.VITE_GITHUB_TOKEN || "")
18+
const [octokit, setOctokit] = useState(() =>
19+
createOctokit(import.meta.env.VITE_GITHUB_TOKEN || "")
20+
)
21+
22+
// Handle token update
23+
const handleTokenUpdate = newToken => {
24+
setToken(newToken)
25+
setOctokit(createOctokit(newToken))
26+
}
27+
28+
return (
29+
<div className="app">
30+
<header>
31+
<h1>GitHub Repository Content Viewer</h1>
32+
<TokenInput token={token} onTokenUpdate={handleTokenUpdate} />
33+
</header>
34+
<main>
35+
<RepoViewer octokit={octokit} />
36+
</main>
37+
<footer>
38+
<p>
39+
Built with React + Vite using{" "}
40+
<a
41+
href="https://docs.github.com/en/rest/repos/contents"
42+
target="_blank"
43+
rel="noopener noreferrer"
44+
>
45+
GitHub Contents API
46+
</a>
47+
</p>
48+
</footer>
49+
</div>
50+
)
51+
}
52+
53+
export default App

‎src/assets/react.svg

+1
Loading

‎src/components/RepoViewer.jsx

+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { useState } from "react"
2+
import PropTypes from "prop-types"
3+
import { FaFolder, FaFile, FaChevronRight } from "react-icons/fa"
4+
5+
/**
6+
* Main component for viewing repository contents
7+
* Handles fetching and displaying both public and private repository contents
8+
* Uses the GitHub Contents API via Octokit
9+
*/
10+
const RepoViewer = ({ octokit }) => {
11+
const [repoPath, setRepoPath] = useState("")
12+
const [currentPath, setCurrentPath] = useState([])
13+
const [contents, setContents] = useState(null)
14+
const [fileContent, setFileContent] = useState(null)
15+
const [error, setError] = useState(null)
16+
const [loading, setLoading] = useState(false)
17+
const [lastApiCall, setLastApiCall] = useState(null)
18+
const [lastApiResponse, setLastApiResponse] = useState(null)
19+
20+
// Format file size for display
21+
const formatSize = bytes => {
22+
if (bytes === 0) return "0 Bytes"
23+
const k = 1024
24+
const sizes = ["Bytes", "KB", "MB", "GB"]
25+
const i = Math.floor(Math.log(bytes) / Math.log(k))
26+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
27+
}
28+
29+
// Format JSON for display
30+
const formatJson = obj => {
31+
return JSON.stringify(obj, null, 2)
32+
}
33+
34+
// Fetch repository contents
35+
const fetchContents = async (owner, repo, path = "") => {
36+
setLoading(true)
37+
setError(null)
38+
setFileContent(null)
39+
40+
const apiCall = {
41+
endpoint: `GET /repos/${owner}/${repo}/contents/${path}`,
42+
parameters: { owner, repo, path },
43+
headers: {
44+
Accept: "application/vnd.github+json",
45+
"X-GitHub-Api-Version": "2022-11-28",
46+
},
47+
}
48+
setLastApiCall(apiCall)
49+
50+
try {
51+
const response = await octokit.repos.getContent({
52+
owner,
53+
repo,
54+
path,
55+
})
56+
57+
setLastApiResponse({
58+
status: response.status,
59+
headers: response.headers,
60+
data: response.data,
61+
})
62+
setContents(
63+
Array.isArray(response.data) ? response.data : [response.data]
64+
)
65+
} catch (err) {
66+
setError(
67+
err.response?.data?.message || "Error fetching repository contents"
68+
)
69+
setContents(null)
70+
setLastApiResponse({
71+
status: err.response?.status,
72+
headers: err.response?.headers,
73+
error: err.response?.data,
74+
})
75+
} finally {
76+
setLoading(false)
77+
}
78+
}
79+
80+
// Fetch file content
81+
const fetchFileContent = async (owner, repo, path) => {
82+
setLoading(true)
83+
setError(null)
84+
85+
const apiCall = {
86+
endpoint: `GET /repos/${owner}/${repo}/contents/${path}`,
87+
parameters: { owner, repo, path },
88+
headers: {
89+
Accept: "application/vnd.github+json",
90+
"X-GitHub-Api-Version": "2022-11-28",
91+
},
92+
}
93+
setLastApiCall(apiCall)
94+
95+
try {
96+
const response = await octokit.repos.getContent({
97+
owner,
98+
repo,
99+
path,
100+
})
101+
102+
setLastApiResponse({
103+
status: response.status,
104+
headers: response.headers,
105+
data: response.data,
106+
})
107+
108+
// For files larger than 1MB, display a message instead of content
109+
if (response.data.size > 1024 * 1024) {
110+
setFileContent({
111+
content:
112+
"File is too large to display. Please download it directly from GitHub.",
113+
name: response.data.name,
114+
})
115+
return
116+
}
117+
118+
// Decode base64 content
119+
const content = atob(response.data.content)
120+
setFileContent({
121+
content,
122+
name: response.data.name,
123+
})
124+
} catch (err) {
125+
setError(err.response?.data?.message || "Error fetching file content")
126+
setLastApiResponse({
127+
status: err.response?.status,
128+
headers: err.response?.headers,
129+
error: err.response?.data,
130+
})
131+
} finally {
132+
setLoading(false)
133+
}
134+
}
135+
136+
// Handle repository path submission
137+
const handleSubmit = async e => {
138+
e.preventDefault()
139+
const [owner, repo] = repoPath.split("/")
140+
141+
if (!owner || !repo) {
142+
setError("Please enter a valid repository path (owner/repo)")
143+
return
144+
}
145+
146+
setCurrentPath([owner, repo])
147+
await fetchContents(owner, repo)
148+
}
149+
150+
// Handle directory navigation
151+
const handleNavigate = async item => {
152+
const [owner, repo] = currentPath
153+
const path = item.path
154+
155+
if (item.type === "dir") {
156+
setCurrentPath([owner, repo, ...path.split("/")])
157+
await fetchContents(owner, repo, path)
158+
} else {
159+
await fetchFileContent(owner, repo, path)
160+
}
161+
}
162+
163+
// Render breadcrumb navigation
164+
const renderBreadcrumb = () => {
165+
if (currentPath.length < 2) return null
166+
167+
return (
168+
<div className="breadcrumb">
169+
{currentPath.map((item, index) => (
170+
<span key={index}>
171+
{index > 0 && <FaChevronRight className="breadcrumb-separator" />}
172+
<button
173+
onClick={() => {
174+
const newPath = currentPath.slice(0, index + 1)
175+
setCurrentPath(newPath)
176+
if (index < 2) {
177+
fetchContents(currentPath[0], currentPath[1])
178+
} else {
179+
fetchContents(
180+
currentPath[0],
181+
currentPath[1],
182+
newPath.slice(2).join("/")
183+
)
184+
}
185+
}}
186+
className="breadcrumb-button"
187+
>
188+
{item}
189+
</button>
190+
</span>
191+
))}
192+
</div>
193+
)
194+
}
195+
196+
return (
197+
<div className="repo-viewer">
198+
<form onSubmit={handleSubmit} className="repo-form">
199+
<input
200+
type="text"
201+
value={repoPath}
202+
onChange={e => setRepoPath(e.target.value)}
203+
placeholder="Enter repository path (e.g., octocat/Hello-World)"
204+
required
205+
/>
206+
<button type="submit">View Repository</button>
207+
</form>
208+
209+
{error && <div className="error">{error}</div>}
210+
{loading && <div className="loading">Loading...</div>}
211+
212+
<div className="viewer-left">
213+
<h2>API Documentation</h2>
214+
{lastApiCall && (
215+
<div className="api-docs">
216+
<div className="api-section">
217+
<h3>Request</h3>
218+
<pre className="code-block">{formatJson(lastApiCall)}</pre>
219+
</div>
220+
{lastApiResponse && (
221+
<div className="api-section">
222+
<h3>Response</h3>
223+
<pre className="code-block">{formatJson(lastApiResponse)}</pre>
224+
</div>
225+
)}
226+
</div>
227+
)}
228+
</div>
229+
230+
<div className="viewer-right">
231+
{currentPath.length > 0 && renderBreadcrumb()}
232+
233+
{contents && !fileContent && (
234+
<div className="contents">
235+
{contents.map(item => (
236+
<div
237+
key={item.sha}
238+
className="content-item"
239+
onClick={() => handleNavigate(item)}
240+
>
241+
{item.type === "dir" ? <FaFolder /> : <FaFile />}
242+
<span className="item-name">{item.name}</span>
243+
{item.type === "file" && (
244+
<span className="item-size">{formatSize(item.size)}</span>
245+
)}
246+
</div>
247+
))}
248+
</div>
249+
)}
250+
251+
{fileContent && (
252+
<div className="file-viewer">
253+
<div className="file-header">
254+
<h3>{fileContent.name}</h3>
255+
<button onClick={() => setFileContent(null)}>Close</button>
256+
</div>
257+
<pre className="file-content">{fileContent.content}</pre>
258+
</div>
259+
)}
260+
</div>
261+
262+
{/* Token Setup Guide */}
263+
<div className="token-guide">
264+
<h2>Setting up GitHub Personal Access Token</h2>
265+
<ol>
266+
<li>
267+
Go to GitHub Settings → Developer settings → Personal access tokens
268+
→ Tokens (classic)
269+
</li>
270+
<li>Click "Generate new token" → "Generate new token (classic)"</li>
271+
<li>Give your token a descriptive name</li>
272+
<li>
273+
Select the following scopes:
274+
<ul>
275+
<li>✓ repo (Full control of private repositories)</li>
276+
<li>
277+
✓ read:packages (Optional, for accessing package contents)
278+
</li>
279+
</ul>
280+
</li>
281+
<li>Click "Generate token" and copy your token immediately</li>
282+
<li>Add the token to the token input field above</li>
283+
</ol>
284+
</div>
285+
</div>
286+
)
287+
}
288+
289+
RepoViewer.propTypes = {
290+
octokit: PropTypes.object.isRequired,
291+
}
292+
293+
export default RepoViewer

‎src/components/TokenInput.jsx

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useState } from "react"
2+
import PropTypes from "prop-types"
3+
4+
/**
5+
* Component for handling GitHub personal access token input
6+
* Allows users to input and update their GitHub token for accessing private repositories
7+
*/
8+
const TokenInput = ({ token, onTokenUpdate }) => {
9+
const [isEditing, setIsEditing] = useState(false)
10+
const [inputToken, setInputToken] = useState(token)
11+
12+
const handleSubmit = e => {
13+
e.preventDefault()
14+
onTokenUpdate(inputToken)
15+
setIsEditing(false)
16+
}
17+
18+
if (!isEditing && !token) {
19+
return (
20+
<div className="token-input">
21+
<button onClick={() => setIsEditing(true)}>
22+
Add GitHub Token (for private repos)
23+
</button>
24+
<small>
25+
Note: You can still access public repositories without a token
26+
</small>
27+
</div>
28+
)
29+
}
30+
31+
if (!isEditing) {
32+
return (
33+
<div className="token-input">
34+
<span>GitHub Token: ••••••••{token.slice(-4)}</span>
35+
<button onClick={() => setIsEditing(true)}>Change</button>
36+
</div>
37+
)
38+
}
39+
40+
return (
41+
<form onSubmit={handleSubmit} className="token-input">
42+
<input
43+
type="password"
44+
value={inputToken}
45+
onChange={e => setInputToken(e.target.value)}
46+
placeholder="Enter GitHub Personal Access Token"
47+
/>
48+
<button type="submit">Save Token</button>
49+
<button type="button" onClick={() => setIsEditing(false)}>
50+
Cancel
51+
</button>
52+
</form>
53+
)
54+
}
55+
56+
TokenInput.propTypes = {
57+
token: PropTypes.string.isRequired,
58+
onTokenUpdate: PropTypes.func.isRequired,
59+
}
60+
61+
export default TokenInput

‎src/index.css

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
:root {
2+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3+
line-height: 1.5;
4+
font-weight: 400;
5+
6+
color-scheme: light dark;
7+
color: rgba(255, 255, 255, 0.87);
8+
background-color: #242424;
9+
10+
font-synthesis: none;
11+
text-rendering: optimizeLegibility;
12+
-webkit-font-smoothing: antialiased;
13+
-moz-osx-font-smoothing: grayscale;
14+
}
15+
16+
a {
17+
font-weight: 500;
18+
color: #646cff;
19+
text-decoration: inherit;
20+
}
21+
a:hover {
22+
color: #535bf2;
23+
}
24+
25+
body {
26+
margin: 0;
27+
display: flex;
28+
place-items: center;
29+
min-width: 320px;
30+
min-height: 100vh;
31+
}
32+
33+
h1 {
34+
font-size: 3.2em;
35+
line-height: 1.1;
36+
}
37+
38+
button {
39+
border-radius: 8px;
40+
border: 1px solid transparent;
41+
padding: 0.6em 1.2em;
42+
font-size: 1em;
43+
font-weight: 500;
44+
font-family: inherit;
45+
background-color: #1a1a1a;
46+
cursor: pointer;
47+
transition: border-color 0.25s;
48+
}
49+
button:hover {
50+
border-color: #646cff;
51+
}
52+
button:focus,
53+
button:focus-visible {
54+
outline: 4px auto -webkit-focus-ring-color;
55+
}
56+
57+
@media (prefers-color-scheme: light) {
58+
:root {
59+
color: #213547;
60+
background-color: #ffffff;
61+
}
62+
a:hover {
63+
color: #747bff;
64+
}
65+
button {
66+
background-color: #f9f9f9;
67+
}
68+
}

‎src/main.jsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StrictMode } from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import './index.css'
4+
import App from './App.jsx'
5+
6+
createRoot(document.getElementById('root')).render(
7+
<StrictMode>
8+
<App />
9+
</StrictMode>,
10+
)

‎vite.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
4+
// https://vite.dev/config/
5+
export default defineConfig({
6+
plugins: [react()],
7+
})

0 commit comments

Comments
 (0)
Please sign in to comment.