Files
2026-09-17 15:32:01 +07:00

349 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* TPU Survey - Core Application Logic
* API communication, Auth state, Toast notifications, Theme
*/
const getAppBasePath = () => {
const p = window.location.pathname;
if (p.includes('/meeting')) return '/meeting';
if (p.includes('/survey')) return '/survey';
return '';
};
const API = {
base: `${getAppBasePath()}/api`,
async request(endpoint, options = {}) {
const url = `${this.base}/${endpoint}`;
const config = {
headers: { 'Content-Type': 'application/json' },
...options
};
if (options.body && typeof options.body === 'object') {
config.body = JSON.stringify(options.body);
}
try {
const res = await fetch(url, config);
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'เกิดข้อผิดพลาด');
return data;
} catch (err) {
if (err.message === 'Failed to fetch') throw new Error('ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้');
throw err;
}
},
get(endpoint) { return this.request(endpoint); },
post(endpoint, body) { return this.request(endpoint, { method: 'POST', body }); },
upload(endpoint, formData) {
const url = `${this.base}/${endpoint}`;
return fetch(url, { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if (data.error) throw new Error(data.error);
return data;
});
}
};
// ============================================================
// Universal Clipboard Helper (HTTP & HTTPS compatible)
// ============================================================
function copyTextToClipboard(text, btnElement) {
if (!text) return;
const showSuccess = () => {
if (typeof Toast !== 'undefined') Toast.success('คัดลอกลิงก์สำเร็จแล้ว!');
if (btnElement) {
const origHtml = btnElement.innerHTML;
btnElement.innerHTML = '✅ คัดลอกแล้ว!';
btnElement.style.backgroundColor = '#16a34a';
btnElement.style.color = '#ffffff';
setTimeout(() => {
btnElement.innerHTML = origHtml;
btnElement.style.backgroundColor = '';
btnElement.style.color = '';
}, 2000);
}
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(showSuccess).catch(() => fallbackCopy(text, showSuccess));
} else {
fallbackCopy(text, showSuccess);
}
}
function fallbackCopy(text, callback) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful && callback) {
callback();
} else if (!successful && typeof Toast !== 'undefined') {
Toast.error('กรุณากดคัดลอกด้วยตนเอง');
}
} catch (err) {
if (typeof Toast !== 'undefined') Toast.error('ไม่สามารถคัดลอกได้');
}
document.body.removeChild(textArea);
}
// ============================================================
// Auth State
// ============================================================
const Auth = {
user: null,
async check() {
try {
const data = await API.get('auth.php?action=check');
if (data.loggedIn) {
this.user = data.user;
return true;
}
} catch (e) {}
this.user = null;
return false;
},
async login(username, password) {
const data = await API.post('auth.php?action=login', { username, password });
this.user = data.user;
return data;
},
async logout() {
await API.post('auth.php?action=logout', {});
this.user = null;
window.location.href = `${getAppBasePath()}/index.html`;
},
isAdmin() { return this.user?.role === 'admin'; },
isTeacher() { return this.user?.role === 'teacher'; }
};
// ============================================================
// Sound Effects (Web Audio API)
// ============================================================
const SoundFX = {
ctx: null,
getCtx() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
return this.ctx;
},
// เสียง OK — 2 โน้ตสั้น ไพเราะ
playSuccess() {
try {
const ctx = this.getCtx();
const now = ctx.currentTime;
// Note 1: C5 (523 Hz)
const osc1 = ctx.createOscillator();
const gain1 = ctx.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(523, now);
gain1.gain.setValueAtTime(0.3, now);
gain1.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc1.connect(gain1);
gain1.connect(ctx.destination);
osc1.start(now);
osc1.stop(now + 0.15);
// Note 2: E5 (659 Hz) — ช้ากว่าเล็กน้อย
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(659, now + 0.1);
gain2.gain.setValueAtTime(0, now);
gain2.gain.setValueAtTime(0.3, now + 0.1);
gain2.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.start(now + 0.1);
osc2.stop(now + 0.3);
// Note 3: G5 (784 Hz) — จบด้วยโน้ตสูง
const osc3 = ctx.createOscillator();
const gain3 = ctx.createGain();
osc3.type = 'sine';
osc3.frequency.setValueAtTime(784, now + 0.2);
gain3.gain.setValueAtTime(0, now);
gain3.gain.setValueAtTime(0.25, now + 0.2);
gain3.gain.exponentialRampToValueAtTime(0.01, now + 0.45);
osc3.connect(gain3);
gain3.connect(ctx.destination);
osc3.start(now + 0.2);
osc3.stop(now + 0.45);
} catch (e) {
// AudioContext not supported
}
},
// เสียง Error — โทนต่ำ
playError() {
try {
const ctx = this.getCtx();
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(150, now + 0.3);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.3);
} catch (e) {}
}
};
// ============================================================
// Toast Notifications
// ============================================================
const Toast = {
container: null,
init() {
if (!this.container) {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
},
show(message, type = 'info', duration = 4000) {
this.init();
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
toast.innerHTML = `<span>${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
this.container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, duration);
},
success(msg) { this.show(msg, 'success'); SoundFX.playSuccess(); },
error(msg) { this.show(msg, 'error'); SoundFX.playError(); },
warning(msg) { this.show(msg, 'warning'); },
info(msg) { this.show(msg, 'info'); }
};
// ============================================================
// Theme Management (Light Mode Only — ม.ตาปี)
// ============================================================
const Theme = {
init() { /* Light mode only */ },
toggle() { /* Disabled */ }
};
// ============================================================
// Modal Helper
// ============================================================
const Modal = {
show(id) {
const el = document.getElementById(id);
if (el) el.classList.add('active');
},
hide(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('active');
},
create(title, bodyHTML, footerHTML = '', size = '') {
const id = 'modal-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.className = 'modal-overlay';
div.innerHTML = `
<div class="modal ${size}">
<div class="modal-header">
<h3>${title}</h3>
<button class="modal-close" onclick="Modal.hide('${id}');this.closest('.modal-overlay').remove();">✕</button>
</div>
<div class="modal-body">${bodyHTML}</div>
${footerHTML ? `<div class="modal-footer">${footerHTML}</div>` : ''}
</div>
`;
document.body.appendChild(div);
div.classList.add('active');
div.addEventListener('click', (e) => {
if (e.target === div) { div.classList.remove('active'); div.remove(); }
});
return id;
}
};
// ============================================================
// Utility Functions
// ============================================================
function formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleDateString('th-TH', { year: 'numeric', month: 'short', day: 'numeric' });
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// ============================================================
// Sidebar Toggle (Mobile)
// ============================================================
function toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar) sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('active');
}
// Close sidebar when clicking overlay
document.addEventListener('click', (e) => {
if (e.target.classList.contains('sidebar-overlay')) {
toggleSidebar();
}
});
// Init
Theme.init();