Skip to content

Commit c7e297c

Browse files
authored
1 parent 345bdc9 commit c7e297c

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

github-issue.html

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>GitHub Issue Viewer</title>
5+
<style>
6+
body {
7+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
8+
max-width: 800px;
9+
margin: 20px auto;
10+
padding: 0 20px;
11+
}
12+
.container {
13+
border: 1px solid #ddd;
14+
border-radius: 8px;
15+
padding: 20px;
16+
}
17+
.input-group {
18+
display: flex;
19+
gap: 10px;
20+
margin-bottom: 20px;
21+
}
22+
.token-group {
23+
margin-bottom: 20px;
24+
}
25+
input {
26+
padding: 8px;
27+
border: 1px solid #ddd;
28+
border-radius: 4px;
29+
font-size: 16px;
30+
}
31+
.url-input {
32+
flex: 1;
33+
}
34+
.token-input {
35+
width: 100%;
36+
max-width: 360px;
37+
}
38+
button {
39+
padding: 8px 16px;
40+
background: #0366d6;
41+
color: white;
42+
border: none;
43+
border-radius: 4px;
44+
cursor: pointer;
45+
font-size: 16px;
46+
}
47+
button:disabled {
48+
background: #ccc;
49+
cursor: not-allowed;
50+
}
51+
.error {
52+
background: #ffebe9;
53+
border: 1px solid #ff8182;
54+
color: #cf222e;
55+
padding: 10px;
56+
border-radius: 4px;
57+
margin-bottom: 20px;
58+
display: none;
59+
}
60+
pre {
61+
background: #f6f8fa;
62+
padding: 16px;
63+
border-radius: 4px;
64+
overflow-x: auto;
65+
white-space: pre-wrap;
66+
word-wrap: break-word;
67+
margin-bottom: 16px;
68+
}
69+
.loading {
70+
display: none;
71+
margin: 20px 0;
72+
color: #666;
73+
}
74+
.copy-button {
75+
display: none;
76+
margin-top: 10px;
77+
}
78+
label {
79+
display: block;
80+
margin-bottom: 8px;
81+
color: #444;
82+
}
83+
.token-help {
84+
font-size: 14px;
85+
color: #666;
86+
margin-top: 4px;
87+
}
88+
</style>
89+
</head>
90+
<body>
91+
<div class="container">
92+
<div class="token-group">
93+
<label for="tokenInput">GitHub Personal Access Token (optional)</label>
94+
<input
95+
type="password"
96+
id="tokenInput"
97+
placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
98+
class="token-input">
99+
<div class="token-help">Token will be saved in your browser for future use</div>
100+
</div>
101+
<div class="input-group">
102+
<input type="text" id="urlInput" placeholder="Enter GitHub issue URL" class="url-input">
103+
<button id="fetchButton">Fetch Issue</button>
104+
</div>
105+
<div id="error" class="error"></div>
106+
<div id="loading" class="loading">Loading...</div>
107+
<pre id="output"></pre>
108+
<button id="copyButton" class="copy-button">Copy to clipboard</button>
109+
</div>
110+
111+
<script>
112+
const urlInput = document.getElementById('urlInput');
113+
const tokenInput = document.getElementById('tokenInput');
114+
const fetchButton = document.getElementById('fetchButton');
115+
const errorDiv = document.getElementById('error');
116+
const loadingDiv = document.getElementById('loading');
117+
const output = document.getElementById('output');
118+
const copyButton = document.getElementById('copyButton');
119+
120+
// Load token from localStorage on page load
121+
document.addEventListener('DOMContentLoaded', () => {
122+
const savedToken = localStorage.getItem('githubToken');
123+
if (savedToken) {
124+
tokenInput.value = savedToken;
125+
}
126+
});
127+
128+
// Save token to localStorage when it changes
129+
tokenInput.addEventListener('input', () => {
130+
const token = tokenInput.value.trim();
131+
if (token) {
132+
localStorage.setItem('githubToken', token);
133+
} else {
134+
localStorage.removeItem('githubToken');
135+
}
136+
});
137+
138+
function convertUrlToApi(githubUrl) {
139+
const match = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/issues\/(\d+)/);
140+
if (!match) throw new Error('Invalid GitHub issue URL');
141+
const [_, owner, repo, issue] = match;
142+
return {
143+
issueUrl: `https://api.github.com/repos/${owner}/${repo}/issues/${issue}`,
144+
commentsUrl: `https://api.github.com/repos/${owner}/${repo}/issues/${issue}/comments`
145+
};
146+
}
147+
148+
function formatDate(dateString) {
149+
return new Date(dateString)
150+
.toISOString()
151+
.replace('T', ' ')
152+
.replace('Z', ' UTC');
153+
}
154+
155+
function getRequestHeaders() {
156+
const headers = {
157+
'Accept': 'application/vnd.github.v3+json'
158+
};
159+
const token = tokenInput.value.trim();
160+
if (token) {
161+
headers['Authorization'] = `token ${token}`;
162+
}
163+
return headers;
164+
}
165+
166+
function copyToClipboard() {
167+
const text = output.textContent;
168+
navigator.clipboard.writeText(text).then(() => {
169+
const originalText = copyButton.textContent;
170+
copyButton.textContent = 'Copied!';
171+
copyButton.disabled = true;
172+
173+
setTimeout(() => {
174+
copyButton.textContent = originalText;
175+
copyButton.disabled = false;
176+
}, 1500);
177+
});
178+
}
179+
180+
function fetchIssueData(inputUrl) {
181+
errorDiv.style.display = 'none';
182+
loadingDiv.style.display = 'block';
183+
output.textContent = '';
184+
copyButton.style.display = 'none';
185+
fetchButton.disabled = true;
186+
187+
let apiUrls;
188+
try {
189+
apiUrls = convertUrlToApi(inputUrl);
190+
} catch (err) {
191+
errorDiv.textContent = err.message;
192+
errorDiv.style.display = 'block';
193+
loadingDiv.style.display = 'none';
194+
fetchButton.disabled = false;
195+
return;
196+
}
197+
198+
const headers = getRequestHeaders();
199+
200+
// Fetch issue data
201+
fetch(apiUrls.issueUrl, { headers })
202+
.then(response => {
203+
if (!response.ok) {
204+
if (response.status === 401) {
205+
throw new Error('Invalid GitHub token');
206+
} else if (response.status === 403) {
207+
throw new Error('Rate limit exceeded. Try adding a GitHub token');
208+
} else if (response.status === 404) {
209+
throw new Error('Issue not found or private repository');
210+
}
211+
throw new Error('Failed to fetch issue');
212+
}
213+
return response.json();
214+
})
215+
.then(issue => {
216+
// Build markdown
217+
let md = `# ${issue.title}\n\n`;
218+
md += `**State:** ${issue.state}\n`;
219+
md += `**Created by:** ${issue.user.login}\n`;
220+
md += `**Created at:** ${formatDate(issue.created_at)}\n\n`;
221+
md += `${issue.body}\n\n`;
222+
223+
// Fetch comments
224+
return fetch(apiUrls.commentsUrl, { headers })
225+
.then(response => {
226+
if (!response.ok) throw new Error('Failed to fetch comments');
227+
return response.json();
228+
})
229+
.then(comments => {
230+
// Add comments to markdown
231+
if (comments.length > 0) {
232+
md += `## Comments\n\n`;
233+
comments.forEach(comment => {
234+
md += `### Comment by ${comment.user.login} at ${formatDate(comment.created_at)}\n\n`;
235+
md += `${comment.body}\n\n`;
236+
});
237+
}
238+
return md;
239+
});
240+
})
241+
.then(markdown => {
242+
output.textContent = markdown;
243+
copyButton.style.display = 'block';
244+
})
245+
.catch(err => {
246+
errorDiv.textContent = err.message;
247+
errorDiv.style.display = 'block';
248+
})
249+
.finally(() => {
250+
loadingDiv.style.display = 'none';
251+
fetchButton.disabled = false;
252+
});
253+
}
254+
255+
fetchButton.addEventListener('click', () => {
256+
fetchIssueData(urlInput.value);
257+
});
258+
259+
urlInput.addEventListener('keypress', (e) => {
260+
if (e.key === 'Enter') {
261+
fetchIssueData(urlInput.value);
262+
}
263+
});
264+
265+
copyButton.addEventListener('click', copyToClipboard);
266+
</script>
267+
</body>
268+
</html>

0 commit comments

Comments
 (0)