Spaces:
Running
Running
Download static/js/index.js from Telearn/Project_Demo: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/Telearn/Project_Demo/resolve/main/static/js/index.js
- Command line
-
hf download hf://spaces/Telearn/Project_Demo/static/js/index.js
-
curl -L -o index.js https://huggingface.co/spaces/Telearn/Project_Demo/resolve/main/static/js/index.js
14.4 kB
| const restrictClass = (window.TELEARN_INDEX && window.TELEARN_INDEX.restrictClass) || ""; | |
| const restrictStudent = (window.TELEARN_INDEX && window.TELEARN_INDEX.restrictStudent) || ""; | |
| let projectData = {}; | |
| let classIds = {}; | |
| let studentIds = {}; | |
| let activeClass = ""; | |
| let activeStudent = ""; | |
| // Resize layout columns dynamically according to restrictions | |
| function applyLayoutRestrictions() { | |
| const container = document.querySelector('.container'); | |
| const panelClasses = document.getElementById('panel-classes'); | |
| const panelStudents = document.getElementById('panel-students'); | |
| if (restrictClass && restrictStudent) { | |
| panelClasses.style.display = 'none'; | |
| panelStudents.style.display = 'none'; | |
| container.style.gridTemplateColumns = '1fr'; | |
| document.querySelector('#panel-projects .panel-header h2').innerText = `${restrictStudent} 的專題列表`; | |
| } else if (restrictClass) { | |
| panelClasses.style.display = 'none'; | |
| container.style.gridTemplateColumns = '1.2fr 1.8fr'; | |
| document.querySelector('#panel-students .panel-header h2').innerText = `${restrictClass} 學生名單`; | |
| } | |
| } | |
| // Fetch data from backend | |
| async function fetchProjects() { | |
| applyLayoutRestrictions(); | |
| try { | |
| let url = '/api/projects'; | |
| const params = []; | |
| if (restrictClass) params.push(`class_name=${encodeURIComponent(restrictClass)}`); | |
| if (restrictStudent) params.push(`student_name=${encodeURIComponent(restrictStudent)}`); | |
| if (params.length > 0) url += '?' + params.join('&'); | |
| const response = await fetch(url); | |
| const res = await response.json(); | |
| projectData = res.projects; | |
| classIds = res.class_ids; | |
| studentIds = res.student_ids; | |
| if (restrictClass && restrictStudent) { | |
| activeClass = restrictClass; | |
| activeStudent = restrictStudent; | |
| renderProjects(); | |
| } else if (restrictClass) { | |
| activeClass = restrictClass; | |
| renderStudents(); | |
| } else { | |
| renderClasses(); | |
| } | |
| // Initialize link generator if elements are present on the page (not in restricted view) | |
| if (document.getElementById("link-generator")) { | |
| initLinkGenerator(); | |
| } | |
| } catch (error) { | |
| console.error("Error fetching projects:", error); | |
| const targetId = (restrictClass && restrictStudent) ? 'project-list' : | |
| (restrictClass ? 'student-list' : 'class-list'); | |
| document.getElementById(targetId).innerHTML = ` | |
| <div class="empty-placeholder" style="color: var(--accent);"> | |
| <p>載入專案清單失敗。請確認伺服器運作正常。</p> | |
| </div>`; | |
| } | |
| } | |
| // Initialize Share Link Generator | |
| function initLinkGenerator() { | |
| const classSelect = document.getElementById("gen-class-select"); | |
| if (!classSelect) return; | |
| classSelect.innerHTML = '<option value="">-- 請選擇班級 --</option>'; | |
| const classes = Object.keys(projectData); | |
| classes.forEach(cls => { | |
| const opt = document.createElement("option"); | |
| opt.value = cls; | |
| opt.textContent = cls; | |
| classSelect.appendChild(opt); | |
| }); | |
| classSelect.onchange = handleGeneratorClassChange; | |
| } | |
| function handleGeneratorClassChange() { | |
| const classSelect = document.getElementById("gen-class-select"); | |
| const studentSelect = document.getElementById("gen-student-select"); | |
| const urlResult = document.getElementById("gen-url-result"); | |
| const selectedClass = classSelect.value; | |
| studentSelect.innerHTML = '<option value="">-- 請選擇學生 (選填) --</option>'; | |
| urlResult.value = ""; | |
| if (!selectedClass) { | |
| studentSelect.disabled = true; | |
| return; | |
| } | |
| studentSelect.disabled = false; | |
| const students = Object.keys(projectData[selectedClass] || {}); | |
| students.forEach(std => { | |
| const opt = document.createElement("option"); | |
| opt.value = std; | |
| opt.textContent = std; | |
| studentSelect.appendChild(opt); | |
| }); | |
| studentSelect.onchange = generateShareUrl; | |
| generateShareUrl(); | |
| } | |
| function generateShareUrl() { | |
| const classSelect = document.getElementById("gen-class-select"); | |
| const studentSelect = document.getElementById("gen-student-select"); | |
| const urlResult = document.getElementById("gen-url-result"); | |
| const selectedClass = classSelect.value; | |
| const selectedStudent = studentSelect.value; | |
| if (!selectedClass) { | |
| urlResult.value = ""; | |
| return; | |
| } | |
| const classId = classIds[selectedClass]; | |
| const baseUrl = window.location.origin; | |
| if (selectedStudent) { | |
| const studentKey = `${selectedClass}/${selectedStudent}`; | |
| const studentId = studentIds[studentKey]; | |
| urlResult.value = `${baseUrl}/student/${studentId}`; | |
| } else { | |
| urlResult.value = `${baseUrl}/class/${classId}`; | |
| } | |
| } | |
| function copyGeneratedUrl() { | |
| const urlResult = document.getElementById("gen-url-result"); | |
| const btnCopy = document.getElementById("btn-copy-url"); | |
| if (!urlResult.value) return; | |
| urlResult.select(); | |
| urlResult.setSelectionRange(0, 99999); | |
| navigator.clipboard.writeText(urlResult.value).then(() => { | |
| const oldText = btnCopy.textContent; | |
| btnCopy.textContent = "已複製!"; | |
| btnCopy.style.background = "#10b981"; | |
| setTimeout(() => { | |
| btnCopy.textContent = oldText; | |
| btnCopy.style.background = ""; | |
| }, 2000); | |
| }).catch(err => { | |
| console.error("Failed to copy:", err); | |
| }); | |
| } | |
| // Render Class Cards | |
| function renderClasses() { | |
| const classList = document.getElementById('class-list'); | |
| classList.innerHTML = ""; | |
| const classes = Object.keys(projectData); | |
| if (classes.length === 0) { | |
| classList.innerHTML = ` | |
| <div class="empty-placeholder"> | |
| <p>目錄下尚無任何學生專案。</p> | |
| <p style="font-size: 0.8rem; color: var(--text-muted);">請將學生專案資料夾置於 projects/ 下</p> | |
| </div>`; | |
| return; | |
| } | |
| classes.forEach(cls => { | |
| const card = document.createElement('div'); | |
| card.className = `item-card ${activeClass === cls ? 'active' : ''}`; | |
| const label = document.createElement('span'); | |
| label.textContent = cls; | |
| card.append(label); | |
| card.insertAdjacentHTML('beforeend', '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 18l6-6-6-6"/></svg>'); | |
| card.onclick = () => selectClass(cls); | |
| classList.appendChild(card); | |
| }); | |
| } | |
| function selectClass(className) { | |
| activeClass = className; | |
| activeStudent = ""; | |
| renderClasses(); | |
| renderStudents(); | |
| document.getElementById('project-list').innerHTML = ` | |
| <div class="empty-placeholder"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke-width="1.5"><path d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653Z" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| <p>請先點選學生以瀏覽其專案</p> | |
| </div>`; | |
| } | |
| // Render Student Cards | |
| function renderStudents() { | |
| const studentList = document.getElementById('student-list'); | |
| studentList.innerHTML = ""; | |
| const students = Object.keys(projectData[activeClass] || {}); | |
| if (students.length === 0) { | |
| studentList.innerHTML = ` | |
| <div class="empty-placeholder"> | |
| <p>無學生資料</p> | |
| </div>`; | |
| return; | |
| } | |
| students.forEach(std => { | |
| const card = document.createElement('div'); | |
| card.className = `item-card ${activeStudent === std ? 'active' : ''}`; | |
| const label = document.createElement('span'); | |
| label.textContent = std; | |
| card.append(label); | |
| card.insertAdjacentHTML('beforeend', '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 18l6-6-6-6"/></svg>'); | |
| card.onclick = () => selectStudent(std); | |
| studentList.appendChild(card); | |
| }); | |
| } | |
| function selectStudent(studentName) { | |
| activeStudent = studentName; | |
| renderStudents(); | |
| renderProjects(); | |
| } | |
| // Render Project Cards with hover-classes matching category colors | |
| function renderProjects() { | |
| const projectList = document.getElementById('project-list'); | |
| projectList.innerHTML = ""; | |
| const projects = (projectData[activeClass] && projectData[activeClass][activeStudent]) || []; | |
| if (projects.length === 0) { | |
| projectList.innerHTML = ` | |
| <div class="empty-placeholder"> | |
| <p>該名學生尚無可執行的專案項目。</p> | |
| </div>`; | |
| return; | |
| } | |
| projects.forEach(proj => { | |
| let tagText = "HTML5 Game"; | |
| let tagClass = "tag-html"; | |
| let hoverClass = "hover-html"; | |
| let btnClass = "btn-launch-html"; | |
| if (proj.type === "python") { | |
| tagText = "Python (Pygame/CV)"; | |
| tagClass = "tag-python"; | |
| hoverClass = "hover-python"; | |
| btnClass = "btn-launch-python"; | |
| } else if (proj.type === "external") { | |
| const url = proj.url.toLowerCase(); | |
| if (url.includes("scratch")) { | |
| tagText = "Scratch Project"; | |
| tagClass = "tag-scratch"; | |
| hoverClass = "hover-scratch"; | |
| btnClass = "btn-launch-scratch"; | |
| } else if (url.includes("makecode")) { | |
| tagText = "MakeCode Arcade"; | |
| tagClass = "tag-makecode"; | |
| hoverClass = "hover-makecode"; | |
| btnClass = "btn-launch-makecode"; | |
| } else { | |
| tagText = "External App"; | |
| tagClass = "tag-html"; | |
| hoverClass = "hover-html"; | |
| btnClass = "btn-launch-html"; | |
| } | |
| } else if (proj.type === "html") { | |
| tagText = "HTML5 Offline"; | |
| tagClass = "tag-html"; | |
| hoverClass = "hover-html"; | |
| btnClass = "btn-launch-html"; | |
| } else if (proj.type === "apk") { | |
| tagText = "Android App (APK)"; | |
| tagClass = "tag-apk"; | |
| hoverClass = "hover-apk"; | |
| btnClass = "btn-launch-apk"; | |
| } | |
| const card = document.createElement('div'); | |
| card.className = `project-card ${hoverClass}`; | |
| const meta = document.createElement('div'); | |
| meta.className = 'project-meta'; | |
| const title = document.createElement('span'); | |
| title.className = 'project-title'; | |
| title.textContent = proj.display_name || proj.name; | |
| const tag = document.createElement('span'); | |
| tag.className = `project-tag ${tagClass}`; | |
| tag.textContent = tagText; | |
| meta.append(title, tag); | |
| const launch = document.createElement('button'); | |
| launch.type = 'button'; | |
| launch.className = `btn-launch ${btnClass}`; | |
| launch.insertAdjacentHTML('afterbegin', '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 3l14 9-14 9V3z"/></svg>'); | |
| launch.append(document.createTextNode('啟動專題專區')); | |
| launch.addEventListener('click', () => launchProject(proj.name)); | |
| card.append(meta, launch); | |
| projectList.appendChild(card); | |
| }); | |
| } | |
| function launchProject(projectName) { | |
| const studentKey = `${activeClass}/${activeStudent}`; | |
| const studentId = studentIds[studentKey] || activeStudent; | |
| const url = `/run/${encodeURIComponent(studentId)}/${encodeURIComponent(projectName)}`; | |
| window.open(url, '_blank'); | |
| } | |
| document.getElementById("btn-copy-url")?.addEventListener("click", copyGeneratedUrl); | |
| window.addEventListener("load", fetchProjects, { once: true }); | |