Project_Demo / static /js /index.js
steveyehtw's picture
Remove showcase numbering and hide MakeCode share-page student name
2de9fdc
Raw History Blame Contribute Delete
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 });