Initial commit - TPU Survey System with Google Forms & Certificate Generator

This commit is contained in:
pongpon pitisuk
2026-09-12 11:56:34 +07:00
commit 72eb7cd0c8
355 changed files with 117012 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
/**
* TPU Survey - Core Application Logic
* API communication, Auth state, Toast notifications, Theme
*/
const API = {
base: '/survey/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;
});
}
};
// ============================================================
// 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 = '/survey/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();