-
-
Notifications
You must be signed in to change notification settings - Fork 1
Claude/main portfolio footer g1 gdx #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Adds a Portfolio navigation item in the Site section of the backend dashboard, allowing easy access to the portfolio page via iframe. https://claude.ai/code/session_0138bAjho1fWwiRZju3nJFJ3
- Create docs.html with markdown parsing via GitHub API - Fix backend: bypass login (Supabase not configured), link to docs.html - Fix portfolio: repo count shows only non-fork repos - Fix footer links: point to ThePhoenixAgency.github.io https://claude.ai/code/session_0138bAjho1fWwiRZju3nJFJ3
- Create viewer.html to display GitHub repos with template styling - Update portfolio to open project links in viewer instead of new tabs - Add section anchors sidebar in docs.html with sticky navigation - Generate anchor IDs from markdown headings https://claude.ai/code/session_0138bAjho1fWwiRZju3nJFJ3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds new in-site pages to view GitHub repository content and project documentation, and wires portfolio/backend navigation to these pages.
Changes:
- Added
viewer.htmlto fetch GitHub repo metadata + README and render it in a styled “viewer” page. - Added
docs.htmlto browse markdown files fromdocs/with a sidebar and generated section anchors. - Updated
portfolio.htmlandbackend.htmlnavigation/links to point to the new viewer/docs pages; adjusted some footer links.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| viewer.html | New repo viewer page that fetches GitHub repo info + README and renders it. |
| docs.html | New documentation browser that fetches markdown docs from GitHub and renders them with anchors. |
| portfolio.html | Routes project links to viewer.html and adjusts GitHub/org stats calculation and footer link. |
| backend.html | Adds Portfolio/Documentation navigation entries and changes session/login behavior. |
| about.html | Updates footer “ThePhoenixAgency” link to external website. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <a href="javascript:history.back()">Back</a> | ||
| <span class="viewer-title" id="viewerTitle">Loading...</span> |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a javascript: URL in an href (Back link) is discouraged and can be blocked by CSP / security scanners. Prefer a normal link target (e.g., portfolio.html) and/or attach a click handler that calls history.back() with a safe fallback when there is no history.
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); | ||
|
|
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Markdown links are emitted as <a href="$2" …> from raw markdown without attribute escaping or URL scheme validation. A malicious doc can inject quotes/attributes or javascript: URLs, resulting in XSS when docsContent.innerHTML is set. Use marked + DOMPurify (already used in readme-viewer.html) or implement strict escaping + allowlist URL schemes (http/https) before generating anchors.
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); | |
| function escapeHtml(str) { | |
| return str.replace(/[&<>"']/g, function (ch) { | |
| switch (ch) { | |
| case '&': return '&'; | |
| case '<': return '<'; | |
| case '>': return '>'; | |
| case '"': return '"'; | |
| case '\'': return '''; | |
| default: return ch; | |
| } | |
| }); | |
| } | |
| function sanitizeUrl(url) { | |
| if (!url) return null; | |
| const trimmed = url.trim(); | |
| // Allow same-page anchors | |
| if (trimmed.startsWith('#')) { | |
| return trimmed; | |
| } | |
| try { | |
| const parsed = new URL(trimmed, window.location.origin); | |
| const protocol = parsed.protocol.toLowerCase(); | |
| if (protocol === 'http:' || protocol === 'https:') { | |
| return parsed.href; | |
| } | |
| } catch (e) { | |
| // Invalid URL, fall through to null | |
| } | |
| return null; | |
| } | |
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => { | |
| const safeUrl = sanitizeUrl(href); | |
| const safeText = escapeHtml(text); | |
| if (!safeUrl) { | |
| // If URL is unsafe or invalid, render just the escaped text | |
| return safeText; | |
| } | |
| return `<a href="${safeUrl}" target="_blank" rel="noopener">${safeText}</a>`; | |
| }); |
| try { | ||
| // Try GitHub API first for rendered markdown | ||
| const apiUrl = `https://api.github.com/repos/${DOCS_CONFIG.owner}/${DOCS_CONFIG.repo}/contents/${DOCS_CONFIG.docsPath}/${filename}`; | ||
|
|
||
| const response = await fetch(apiUrl, { | ||
| headers: { | ||
| 'Accept': 'application/vnd.github.v3.raw' | ||
| } |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment says “Try GitHub API first for rendered markdown”, but the request uses Accept: application/vnd.github.v3.raw which returns raw markdown. Update the comment to match behavior (raw fetch + local render) to avoid confusion when maintaining this code.
| // Check if already logged in - bypass login for now | ||
| function checkSession() { | ||
| const session = localStorage.getItem('supabase_session'); | ||
| if (session) { | ||
| showDashboard(); | ||
| } | ||
| // Always show dashboard (Supabase not configured) | ||
| showDashboard(); | ||
| } |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkSession() now always calls showDashboard(), bypassing the login overlay entirely. This makes backend.html effectively unauthenticated even if/when Supabase is later configured, and can expose admin/dashboard UI publicly. Gate this behind an explicit dev flag, or keep the session check and require login when no valid session exists.
|
|
||
| <div class="social-links"> | ||
| <a href="https://github.com/ThePhoenixAgency" target="_blank" class="social-link"> | ||
| <a href="viewer.html?url=https://github.com/ThePhoenixAgency" class="social-link"> |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GitHub social link now routes to viewer.html with an org/user URL (https://github.com/ThePhoenixAgency), but viewer.html only supports GitHub repository URLs (owner/repo) and will show “Unsupported URL”. Either keep this link pointing directly to GitHub (as before), or extend viewer.html to support org/user pages (or pass a supported repo URL).
| <a href="viewer.html?url=https://github.com/ThePhoenixAgency" class="social-link"> | |
| <a href="https://github.com/ThePhoenixAgency" target="_blank" class="social-link"> |
| // Fetch repos first (single API call for all data) | ||
| const reposResponse = await fetch(`https://api.github.com/orgs/${PORTFOLIO_CONFIG.githubOrg}/repos?per_page=100`); | ||
| if (!reposResponse.ok) throw new Error('Failed to fetch repos'); | ||
|
|
||
| let repos = await reposResponse.json(); | ||
| if (!Array.isArray(repos)) throw new Error('Invalid response'); | ||
|
|
||
| // Calculate total stars | ||
| // Filter: hide forks FIRST before counting | ||
| if (PORTFOLIO_CONFIG.hideForks) { | ||
| repos = repos.filter(r => !r.fork); | ||
| } | ||
|
|
||
| // Now count repos (only non-forks) | ||
| const reposEl = document.getElementById('totalRepos'); | ||
| if (reposEl) reposEl.textContent = String(repos.length); | ||
|
|
||
| // Calculate total stars (only from non-fork repos) | ||
| const totalStars = repos.reduce((sum, r) => sum + (r.stargazers_count || 0), 0); | ||
| const starsEl = document.getElementById('totalStars'); | ||
| if (starsEl) starsEl.textContent = String(totalStars); |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repository count (and derived totals like stars) are computed from the first page of repos only (per_page=100). For orgs/users with >100 repos, the displayed totals will be incorrect. Consider using the org endpoint’s public_repos for the count and either paginate for star totals or clearly label these as “top 100”.
| // Parse markdown to HTML | ||
| function parseMarkdown(md) { | ||
| let html = md; | ||
| html = html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); | ||
| html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>'); | ||
| html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); | ||
| html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>'); | ||
| html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>'); | ||
| html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>'); | ||
| html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); | ||
| html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); | ||
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>'); | ||
| html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>'); |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Markdown links are converted into <a href="$2" …> using raw URL text from the README. Because the URL isn’t attribute-escaped or validated, a crafted markdown link can inject attributes (quotes) or use javascript: URLs, leading to XSS. Use a real markdown parser + sanitizer (e.g., marked + DOMPurify as used in readme-viewer.html) or strictly validate/escape link URLs before inserting into innerHTML.
| viewerContent.innerHTML = ` | ||
| <h1>${repoData.name}</h1> | ||
| <p>${repoData.description || 'No description'}</p> | ||
| <div class="repo-stats"> | ||
| <span class="repo-stat">Stars: ${repoData.stargazers_count}</span> | ||
| <span class="repo-stat">Forks: ${repoData.forks_count}</span> | ||
| <span class="repo-stat">Language: ${repoData.language || 'N/A'}</span> | ||
| <span class="repo-stat">Updated: ${new Date(repoData.updated_at).toLocaleDateString()}</span> | ||
| </div> | ||
| <hr style="border-color: rgba(255,255,255,0.1); margin: 30px 0;"> | ||
| ${readmeHtml || '<p>No README available.</p>'} | ||
| `; |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This template assigns viewerContent.innerHTML with unescaped data coming from the GitHub API (repoData.name/description) plus generated README HTML. Since viewer.html accepts arbitrary repo URLs via query params, this is effectively untrusted input and can enable XSS. Prefer building DOM nodes with textContent for API fields, and sanitize any rendered markdown before insertion.
No description provided.