Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Project Details</title> | |
| <!-- Iconify --> | |
| <script src="https://code.iconify.design/3/3.1.1/iconify.min.js"></script> | |
| <!-- Marked.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #0f0f0f; | |
| --card: #1a1a1a; | |
| --accent: #f5c542; | |
| --text: #e5e5e5; | |
| --muted: #9e9e9e; | |
| --divider: #2a2a2a; | |
| --error: #ff5252; | |
| --radius: 20px; | |
| --shadow: 0 10px 30px rgba(0,0,0,0.6); | |
| --font: "Segoe UI", Roboto, sans-serif; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--font); | |
| line-height: 1.7; | |
| } | |
| .page { | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| padding: 60px 16px; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 1000px; | |
| background: var(--card); | |
| border-radius: var(--radius); | |
| padding: 50px 40px; | |
| box-shadow: var(--shadow); | |
| animation: fadeIn 0.4s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| h1 { | |
| margin: 0; | |
| text-align: center; | |
| font-size: 36px; | |
| color: var(--accent); | |
| } | |
| .section { | |
| margin-top: 50px; | |
| } | |
| .section-title { | |
| text-align: center; | |
| font-size: 14px; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| margin-bottom: 20px; | |
| } | |
| .divider { | |
| height: 1px; | |
| background: var(--divider); | |
| margin: 40px 0; | |
| opacity: 0.6; | |
| } | |
| .category { | |
| text-align: center; | |
| color: var(--muted); | |
| } | |
| .tools { | |
| display: flex; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| gap: 30px; | |
| } | |
| .tool { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| font-size: 14px; | |
| color: var(--muted); | |
| transition: transform 0.3s; | |
| } | |
| .tool:hover { transform: translateY(-4px); } | |
| .tool .iconify { | |
| font-size: 40px; | |
| color: var(--accent); | |
| margin-bottom: 8px; | |
| } | |
| .media { | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .media video, | |
| .media img { | |
| width: 100%; | |
| max-width: 750px; | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| } | |
| .description { | |
| max-width: 800px; | |
| margin: auto; | |
| } | |
| .description h1, | |
| .description h2, | |
| .description h3 { | |
| color: var(--accent); | |
| } | |
| .description pre { | |
| background: #111; | |
| padding: 15px; | |
| border-radius: 10px; | |
| overflow-x: auto; | |
| } | |
| .loading { | |
| text-align: center; | |
| color: var(--muted); | |
| } | |
| .error { | |
| text-align: center; | |
| color: var(--error); | |
| } | |
| @media (max-width: 768px) { | |
| .container { padding: 30px 20px; } | |
| h1 { font-size: 28px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="page"> | |
| <div class="container"> | |
| <h1 id="title"></h1> | |
| <div class="section"> | |
| <div class="section-title">Category</div> | |
| <div class="category" id="category"></div> | |
| </div> | |
| <div class="divider"></div> | |
| <div class="section"> | |
| <div class="section-title">Tools Used</div> | |
| <div class="tools" id="tools"></div> | |
| </div> | |
| <div class="divider"></div> | |
| <div class="section"> | |
| <div class="section-title">Preview</div> | |
| <div class="media" id="media"></div> | |
| </div> | |
| <div class="divider"></div> | |
| <div class="section"> | |
| <div class="section-title">Description</div> | |
| <div id="description" class="description loading"> | |
| Loading README... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const toolIcons = { | |
| python: "logos:python", | |
| tensorflow: "logos:tensorflow", | |
| opencv: "logos:opencv", | |
| fastapi: "logos:fastapi-icon", | |
| nextjs: "logos:nextjs-icon", | |
| react: "logos:react", | |
| javascript: "logos:javascript", | |
| docker: "logos:docker-icon", | |
| linux: "logos:linux-tux" | |
| }; | |
| document.addEventListener("DOMContentLoaded", init); | |
| async function init() { | |
| const params = new URLSearchParams(window.location.search); | |
| const dataFile = params.get("data"); // e.g., "data/Pro1.json" | |
| if (!dataFile) return showError("No project file provided."); | |
| let project; | |
| try { | |
| const res = await fetch(dataFile); | |
| if (!res.ok) throw new Error(); | |
| project = await res.json(); | |
| } catch { | |
| return showError("Failed to load project JSON."); | |
| } | |
| renderBasicInfo(project); | |
| renderMedia(project); | |
| renderTools(project.tools); | |
| if (!project.description) return showError("README file path missing."); | |
| const descPath = project.description.trim(); | |
| if (!descPath.endsWith(".md") && !descPath.endsWith(".readme")) { | |
| return showError("Description must be a .md or .readme file."); | |
| } | |
| await loadReadme(descPath); | |
| } | |
| function renderBasicInfo(project) { | |
| document.getElementById("title").textContent = | |
| project.name || "Untitled Project"; | |
| document.getElementById("category").textContent = | |
| project.category || "N/A"; | |
| } | |
| function renderMedia(project) { | |
| const media = document.getElementById("media"); | |
| media.innerHTML = ""; | |
| if (!project.filePath) return; | |
| if (project.displayType === "video") { | |
| const video = document.createElement("video"); | |
| video.src = project.filePath; | |
| video.controls = true; | |
| media.appendChild(video); | |
| } else { | |
| const img = document.createElement("img"); | |
| img.src = project.filePath; | |
| img.alt = project.name || "Project preview"; | |
| media.appendChild(img); | |
| } | |
| } | |
| function renderTools(toolsText = "") { | |
| const container = document.getElementById("tools"); | |
| container.innerHTML = ""; | |
| toolsText | |
| .split(/[, ]+/) | |
| .filter(Boolean) | |
| .forEach(tool => { | |
| const key = tool.toLowerCase(); | |
| const icon = toolIcons[key] || "mdi:tools"; | |
| const el = document.createElement("div"); | |
| el.className = "tool"; | |
| el.innerHTML = ` | |
| <span class="iconify" data-icon="${icon}"></span> | |
| <span>${tool}</span> | |
| `; | |
| container.appendChild(el); | |
| }); | |
| } | |
| async function loadReadme(path) { | |
| const description = document.getElementById("description"); | |
| try { | |
| const response = await fetch(path); | |
| if (!response.ok) throw new Error(); | |
| const markdown = await response.text(); | |
| description.classList.remove("loading"); | |
| description.innerHTML = marked.parse(markdown); | |
| } catch { | |
| showError("Failed to load README file."); | |
| } | |
| } | |
| function showError(message) { | |
| const description = document.getElementById("description"); | |
| description.classList.remove("loading"); | |
| description.innerHTML = `<div class="error">${message}</div>`; | |
| } | |
| </script> | |
| </body> | |
| </html> |