Initial commit - TPU Survey System with Google Forms & Certificate Generator
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user