|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="UTF-8"> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | +<title>GitHub HTML Visualizer</title> |
| 7 | +<style> |
| 8 | + * { box-sizing: border-box; margin: 0; padding: 0; } |
| 9 | + body { |
| 10 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| 11 | + background: #0d1117; |
| 12 | + color: #e6edf3; |
| 13 | + min-height: 100vh; |
| 14 | + display: flex; |
| 15 | + flex-direction: column; |
| 16 | + } |
| 17 | + header { |
| 18 | + padding: 18px 24px; |
| 19 | + background: #161b22; |
| 20 | + border-bottom: 1px solid #30363d; |
| 21 | + display: flex; |
| 22 | + align-items: center; |
| 23 | + gap: 12px; |
| 24 | + } |
| 25 | + header h1 { font-size: 16px; font-weight: 600; } |
| 26 | + header span { font-size: 13px; color: #8b949e; } |
| 27 | + .star-btn { |
| 28 | + margin-left: auto; |
| 29 | + display: flex; |
| 30 | + align-items: center; |
| 31 | + gap: 6px; |
| 32 | + background: #21262d; |
| 33 | + border: 1px solid #30363d; |
| 34 | + color: #e6edf3; |
| 35 | + border-radius: 6px; |
| 36 | + padding: 5px 12px; |
| 37 | + font-size: 13px; |
| 38 | + font-weight: 600; |
| 39 | + text-decoration: none; |
| 40 | + transition: background .2s, border-color .2s; |
| 41 | + } |
| 42 | + .star-btn:hover { background: #30363d; border-color: #8b949e; } |
| 43 | + .star-btn svg { fill: #e3b341; } |
| 44 | + .input-bar { |
| 45 | + padding: 16px 24px; |
| 46 | + background: #161b22; |
| 47 | + border-bottom: 1px solid #30363d; |
| 48 | + display: flex; |
| 49 | + gap: 10px; |
| 50 | + } |
| 51 | + .url-input { |
| 52 | + flex: 1; |
| 53 | + background: #0d1117; |
| 54 | + border: 1px solid #30363d; |
| 55 | + border-radius: 6px; |
| 56 | + padding: 8px 14px; |
| 57 | + color: #e6edf3; |
| 58 | + font-size: 14px; |
| 59 | + outline: none; |
| 60 | + transition: border-color .2s; |
| 61 | + } |
| 62 | + .url-input::placeholder { color: #484f58; } |
| 63 | + .url-input:focus { border-color: #388bfd; } |
| 64 | + .btn { |
| 65 | + background: #238636; |
| 66 | + color: #fff; |
| 67 | + border: none; |
| 68 | + border-radius: 6px; |
| 69 | + padding: 8px 20px; |
| 70 | + font-size: 14px; |
| 71 | + font-weight: 600; |
| 72 | + cursor: pointer; |
| 73 | + transition: background .2s; |
| 74 | + } |
| 75 | + .btn:hover { background: #2ea043; } |
| 76 | + .btn:disabled { background: #21262d; color: #484f58; cursor: not-allowed; } |
| 77 | + .status-bar { |
| 78 | + padding: 6px 24px; |
| 79 | + font-size: 12px; |
| 80 | + min-height: 28px; |
| 81 | + display: flex; |
| 82 | + align-items: center; |
| 83 | + gap: 8px; |
| 84 | + } |
| 85 | + .status-bar.error { color: #f85149; } |
| 86 | + .status-bar.success{ color: #3fb950; } |
| 87 | + .status-bar.loading{ color: #58a6ff; } |
| 88 | + .spinner { |
| 89 | + width: 12px; height: 12px; |
| 90 | + border: 2px solid #30363d; |
| 91 | + border-top-color: #58a6ff; |
| 92 | + border-radius: 50%; |
| 93 | + animation: spin .7s linear infinite; |
| 94 | + display: none; |
| 95 | + } |
| 96 | + @keyframes spin { to { transform: rotate(360deg); } } |
| 97 | + .api-badge { |
| 98 | + padding: 4px 24px; |
| 99 | + background: #161b22; |
| 100 | + border-bottom: 1px solid #30363d; |
| 101 | + font-size: 11px; |
| 102 | + color: #484f58; |
| 103 | + display: none; |
| 104 | + } |
| 105 | + .api-badge a { color: #58a6ff; text-decoration: none; font-family: monospace; font-size: 11px; } |
| 106 | + .api-badge a:hover { text-decoration: underline; } |
| 107 | + iframe { |
| 108 | + flex: 1; |
| 109 | + border: none; |
| 110 | + display: none; |
| 111 | + min-height: calc(100vh - 130px); |
| 112 | + background: #fff; |
| 113 | + } |
| 114 | + .placeholder { |
| 115 | + flex: 1; |
| 116 | + display: flex; |
| 117 | + flex-direction: column; |
| 118 | + align-items: center; |
| 119 | + justify-content: center; |
| 120 | + gap: 14px; |
| 121 | + color: #484f58; |
| 122 | + background: #0d1117; |
| 123 | + min-height: calc(100vh - 130px); |
| 124 | + } |
| 125 | + .placeholder svg { opacity: .25; } |
| 126 | + .placeholder p { font-size: 14px; } |
| 127 | + .placeholder code { |
| 128 | + font-size: 12px; background: #161b22; |
| 129 | + padding: 6px 14px; border-radius: 6px; |
| 130 | + border: 1px solid #30363d; color: #8b949e; font-family: monospace; |
| 131 | + } |
| 132 | +</style> |
| 133 | +</head> |
| 134 | +<body> |
| 135 | + |
| 136 | +<header> |
| 137 | + <svg width="22" height="22" viewBox="0 0 24 24" fill="#e6edf3"> |
| 138 | + <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/> |
| 139 | + </svg> |
| 140 | + <h1>GitHub HTML Visualizer</h1> |
| 141 | + <span>— paste any GitHub .html blob URL to preview it live</span> |
| 142 | + <a class="star-btn" href="https://github.com/ghviz/github-html-viewer" target="_blank"> |
| 143 | + <svg width="15" height="15" viewBox="0 0 24 24"><path d="M12 .587l3.668 7.431 8.2 1.192-5.934 5.782 1.4 8.168L12 18.896l-7.334 3.864 1.4-8.168L.132 9.21l8.2-1.192z"/></svg> |
| 144 | + Star on GitHub |
| 145 | + </a> |
| 146 | +</header> |
| 147 | + |
| 148 | +<div class="input-bar"> |
| 149 | + <input id="urlInput" class="url-input" type="text" |
| 150 | + placeholder="https://github.com/user/repo/blob/main/file.html" |
| 151 | + value="https://github.com/shanraisshan/claude-code-best-practice/blob/main/presentation/index.html"/> |
| 152 | + <button class="btn" id="loadBtn" onclick="loadPreview()">Render ▶</button> |
| 153 | +</div> |
| 154 | + |
| 155 | +<div class="status-bar" id="statusBar"> |
| 156 | + <div class="spinner" id="spinner"></div> |
| 157 | + <span id="statusText">Enter a GitHub HTML file URL above and click Render.</span> |
| 158 | +</div> |
| 159 | + |
| 160 | +<div class="api-badge" id="apiBadge"> |
| 161 | + 📡 Via GitHub API: <a id="apiLink" href="#" target="_blank"></a> |
| 162 | +</div> |
| 163 | + |
| 164 | +<div class="placeholder" id="placeholder"> |
| 165 | + <svg width="60" height="60" viewBox="0 0 24 24" fill="#e6edf3"> |
| 166 | + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z"/> |
| 167 | + </svg> |
| 168 | + <p>No preview yet</p> |
| 169 | + <code>github.com/user/repo/blob/branch/file.html</code> |
| 170 | +</div> |
| 171 | + |
| 172 | +<iframe id="previewFrame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe> |
| 173 | + |
| 174 | +<script> |
| 175 | + document.getElementById('urlInput').addEventListener('keydown', e => { |
| 176 | + if (e.key === 'Enter') loadPreview(); |
| 177 | + }); |
| 178 | + |
| 179 | + // Parse a GitHub blob URL → { apiUrl, rawUrl } |
| 180 | + function parseGitHubUrl(input) { |
| 181 | + try { |
| 182 | + const url = new URL(input.trim()); |
| 183 | + if (!url.hostname.includes('github.com')) return null; |
| 184 | + const m = url.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/); |
| 185 | + if (!m) return null; |
| 186 | + const [, owner, repo, branch, path] = m; |
| 187 | + return { |
| 188 | + apiUrl: `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`, |
| 189 | + rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}` |
| 190 | + }; |
| 191 | + } catch { return null; } |
| 192 | + } |
| 193 | + |
| 194 | + function setStatus(msg, type = '') { |
| 195 | + const bar = document.getElementById('statusBar'); |
| 196 | + document.getElementById('statusText').textContent = msg; |
| 197 | + document.getElementById('spinner').style.display = type === 'loading' ? 'block' : 'none'; |
| 198 | + bar.className = 'status-bar' + (type ? ' ' + type : ''); |
| 199 | + } |
| 200 | + |
| 201 | + async function loadPreview() { |
| 202 | + const parsed = parseGitHubUrl(document.getElementById('urlInput').value); |
| 203 | + const badge = document.getElementById('apiBadge'); |
| 204 | + const link = document.getElementById('apiLink'); |
| 205 | + const frame = document.getElementById('previewFrame'); |
| 206 | + const ph = document.getElementById('placeholder'); |
| 207 | + const btn = document.getElementById('loadBtn'); |
| 208 | + |
| 209 | + if (!parsed) { |
| 210 | + setStatus('⚠ Invalid URL — must be a GitHub blob URL: github.com/user/repo/blob/branch/file.html', 'error'); |
| 211 | + return; |
| 212 | + } |
| 213 | + |
| 214 | + // Show API badge |
| 215 | + badge.style.display = 'block'; |
| 216 | + link.href = link.textContent = parsed.apiUrl; |
| 217 | + |
| 218 | + btn.disabled = true; |
| 219 | + setStatus('Fetching via GitHub API…', 'loading'); |
| 220 | + ph.style.display = 'none'; |
| 221 | + frame.style.display = 'none'; |
| 222 | + |
| 223 | + try { |
| 224 | + const res = await fetch(parsed.apiUrl, { |
| 225 | + headers: { Accept: 'application/vnd.github.v3+json' } |
| 226 | + }); |
| 227 | + |
| 228 | + if (res.status === 403) throw new Error('Rate limit hit — try again in a minute, or the repo may be private.'); |
| 229 | + if (res.status === 404) throw new Error('File not found — check the URL.'); |
| 230 | + if (!res.ok) throw new Error(`GitHub API error ${res.status}`); |
| 231 | + |
| 232 | + const json = await res.json(); |
| 233 | + if (!json.content) throw new Error('No content returned from GitHub API.'); |
| 234 | + |
| 235 | + // GitHub returns base64 with newlines — strip them then decode |
| 236 | + const html = decodeURIComponent( |
| 237 | + atob(json.content.replace(/\n/g, '')) |
| 238 | + .split('') |
| 239 | + .map(c => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')) |
| 240 | + .join('') |
| 241 | + ); |
| 242 | + |
| 243 | + frame.srcdoc = html; |
| 244 | + frame.style.display = 'block'; |
| 245 | + setStatus(`✓ Rendered successfully — ${(html.length / 1024).toFixed(1)} KB`, 'success'); |
| 246 | + } catch (err) { |
| 247 | + ph.style.display = 'flex'; |
| 248 | + setStatus(`✗ ${err.message}`, 'error'); |
| 249 | + } finally { |
| 250 | + btn.disabled = false; |
| 251 | + } |
| 252 | + } |
| 253 | +</script> |
| 254 | +</body> |
| 255 | +</html> |
0 commit comments