📂 FileMgr
📍
/home/u625410797/domains/thaiyouth.org/public_html/js
✏️ Edit File: /home/u625410797/domains/thaiyouth.org/public_html/js/app.js
⬅ Kembali
const API_BASE = window.location.origin + '/api'; const API_AUTH = `${API_BASE}/auth.php`; const API_DATA = `${API_BASE}/data.php`; console.log('🚀 Thai Youth System Initialized'); console.log('📡 API Base:', API_BASE); // ========================================== // STATE MANAGEMENT // ========================================== let state = { user: null, token: localStorage.getItem('token') || null, currentRoute: window.location.pathname, menus: [], settings: {}, lang: localStorage.getItem('lang') || 'th', translations: { th: { home: 'หน้าแรก', about: 'เกี่ยวกับเรา', programs: 'โครงการ', news: 'ข่าวสาร', gallery: 'แกลเลอรี', volunteer: 'อาสาสมัคร', contact: 'ติดต่อเรา', donate: 'ร่วมบริจาค', login: 'เข้าสู่ระบบ', hero_title: 'สร้างอนาคตที่ยั่งยืน ผ่านพลังของเยาวชนไทย', hero_sub: 'ร่วมกันสนับสนุนและพัฒนาศักยภาพเยาวชนไทย เพื่อสังคมที่น่าอยู่และยั่งยืนสำหรับทุกคน', featured_progs: 'โครงการเด่น', view_all: 'ดูทั้งหมด', funding_gap: 'ช่องว่างแห่งความหวัง', donate_now: 'บริจาคเดี๋ยวนี้', read_more: 'อ่านเพิ่มเติม', contact_us: 'ติดต่อเรา', foot_about: 'เราเชื่อว่าเยาวชนคือพลังสำคัญที่จะเปลี่ยนแปลงสังคมให้ดีขึ้น', raised: 'ระดมทุนได้แล้ว', goal: 'เป้าหมาย' }, en: { home: 'Home', about: 'About', programs: 'Programs', news: 'News', gallery: 'Gallery', volunteer: 'Volunteer', contact: 'Contact', donate: 'Donate', login: 'Login', hero_title: 'Connecting Hearts, Building Futures', hero_sub: 'Empowering Thai youth to lead and innovate for a better tomorrow.', featured_progs: 'Featured Programs', view_all: 'View All', funding_gap: 'Funding Gap', donate_now: 'Donate Now', read_more: 'Read More', contact_us: 'Contact Us', foot_about: 'We believe youth are the key power to change society for the better.', raised: 'Raised', goal: 'Goal' } } }; // ========================================== // INITIALIZATION // ========================================== document.addEventListener('DOMContentLoaded', async () => { initUI(); // Core Initial Data await Promise.all([ fetchSettings(), fetchMenus(), state.token ? fetchUser() : Promise.resolve() ]); handleRoute(); window.addEventListener('popstate', handleRoute); // Initialize Lang UI updateLangUI(); updateStaticLabels(); // Global Event Delegation for Links document.body.addEventListener('click', e => { const link = e.target.closest('a'); if (link && link.href.startsWith(window.location.origin) && !link.hasAttribute('target') && !link.classList.contains('no-route')) { e.preventDefault(); navigateTo(new URL(link.href).pathname); } }); }); // ========================================== // ROUTER (Improved for Dynamic Slugs) // ========================================== const routes = { '/': renderHome, '/about': renderAbout, '/programs': renderPrograms, '/news': renderNews, '/gallery': renderGallery, '/volunteer': renderVolunteer, '/contact': renderContact, '/donate': renderDonate, '/login': () => { openModal('login'); navigateTo('/'); } }; function navigateTo(path) { if (state.currentRoute === path) return; window.history.pushState({}, '', path); state.currentRoute = path; handleRoute(); } async function handleRoute() { const app = document.getElementById('app'); const path = state.currentRoute; // 1. Update Active Navigation document.querySelectorAll('#nav-list a').forEach(a => { const href = a.getAttribute('href'); if (href === path || (path.startsWith(href) && href !== '/')) a.classList.add('active'); else a.classList.remove('active'); }); // 2. Loading State with Animation app.innerHTML = ` <div class="loading-screen" style="animation: fadeIn 0.3s ease-out"> <div class="spinner"></div> </div> `; try { // 3. Dynamic Route Matching let renderFunc = routes[path]; // Handle Program Details /programs/:slug if (!renderFunc && path.startsWith('/programs/')) { const slug = path.split('/')[2]; renderFunc = (app) => renderProgramDetail(app, slug); } // Handle News Details /news/:slug if (!renderFunc && path.startsWith('/news/')) { const slug = path.split('/')[2]; renderFunc = (app) => renderNewsDetail(app, slug); } // Handle CMS Pages (Custom Slugs) if (!renderFunc) { const cmsPage = await fetchCMSPage(path.substring(1)); if (cmsPage) { renderFunc = (app) => renderCMSPage(app, cmsPage); } } if (renderFunc) { window.scrollTo({ top: 0, behavior: 'smooth' }); await renderFunc(app); initAnimations(); // Apply entrance animations } else { renderNotFound(app); } } catch (err) { console.error('Route error:', err); app.innerHTML = ` <div class="container section text-center"> <div class="card card-body" style="border-top: 4px solid var(--red);"> <h2 class="text-primary">เกิดข้อผิดพลาด</h2> <p>${err.message}</p> <button class="btn btn-primary mt-3" onclick="location.reload()">โหลดใหม่</button> </div> </div>`; } } // ========================================== // DATA FETCHERS // ========================================== async function fetchSettings() { try { const url = `${API_DATA}?endpoint=settings`; console.log('📡 Fetching Settings:', url); const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status} at ${url}`); const data = await res.json(); if (data.success) { Object.entries(data.data).forEach(([key, value]) => state.settings[key] = value); updateGlobalUI(); } else { console.warn('Settings success=false:', data.message); } } catch (err) { console.error('Settings fetch failed:', err); // Fallback to default name if settings fail document.title = 'Thai Youth Foundation'; } } async function fetchMenus() { try { const url = `${API_DATA}?endpoint=menus`; console.log('📡 Fetching Menus:', url); const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status} at ${url}`); const data = await res.json(); if (data.success) { state.menus = data.data; renderNav(); updateStaticLabels(); } } catch (err) { console.error('Menus fetch failed:', err); } } function toggleLang(l) { state.lang = l; localStorage.setItem('lang', l); updateLangUI(); renderNav(); updateStaticLabels(); handleRoute(); // Re-render current page } function updateLangUI() { document.querySelectorAll('.lang-switch button').forEach(btn => btn.classList.remove('active')); const activeBtn = document.getElementById(`lang-${state.lang}`); if(activeBtn) activeBtn.classList.add('active'); } function t(key) { return state.translations[state.lang][key] || key; } function updateStaticLabels() { // Footer & Common Labels document.querySelectorAll('[data-t]').forEach(el => { const key = el.dataset.t; el.textContent = t(key); }); const donateBtn = document.getElementById('donate-btn'); if(donateBtn) donateBtn.textContent = (state.lang === 'th' ? '❤️ บริจาค' : '❤️ Donate'); } async function fetchCMSPage(slug) { if (!slug) return null; try { const res = await fetch(`${API_DATA}?endpoint=pages&slug=${slug}`); const data = await res.json(); return data.success ? data.data : null; } catch { return null; } } // ========================================== // PAGE RENDERERS // ========================================== function renderNav() { const navList = document.getElementById('nav-list'); if (!navList || !state.menus) return; navList.innerHTML = state.menus.map(m => { const title = state.lang === 'th' ? m.title : (m.title_en || m.title); if (m.children && m.children.length > 0) { return ` <li class="nav-item-dropdown"> <a href="${m.url}" class="dropdown-trigger">${title} ▾</a> <ul class="dropdown-menu"> ${m.children.map(c => ` <li><a href="${c.url}">${state.lang === 'th' ? c.title : (c.title_en || c.title)}</a></li> `).join('')} </ul> </li> `; } return `<li><a href="${m.url}">${title}</a></li>`; }).join(''); } async function renderHome(app) { app.innerHTML = ` <section class="hero reveal"> <div class="container"> <div class="hero-content"> <div class="hero-badge animate-float">✨ ${state.settings.site_name || 'Thai Youth Foundation'}</div> <h1 class="animate-up">${state.lang === 'th' ? (state.settings.hero_title || t('hero_title')) : (state.settings.hero_title_en || t('hero_title'))}</h1> <p class="animate-up" style="animation-delay:0.1s">${state.lang === 'th' ? (state.settings.hero_subtitle || t('hero_sub')) : (state.settings.hero_subtitle_en || t('hero_sub'))}</p> <div class="hero-actions animate-up" style="animation-delay:0.2s"> <a href="/donate" class="btn btn-accent" style="padding: 14px 32px; font-size: 1.1rem;">❤️ ${t('donate')}</a> <a href="/about" class="btn btn-outline" style="color:#fff; border-color:rgba(255,255,255,0.6); padding: 14px 32px; font-size: 1.1rem; background: rgba(255,255,255,0.05);">${t('read_more')}</a> </div> </div> </div> </section> <section class="section"> <div class="container"> <div class="news-portal-grid"> <div class="news-main"> <h3 class="sidebar-title">🌟 โครงการเด่น (Featured Projects)</h3> <div id="featured-programs" class="grid-2"> ${Array(2).fill('<div class="skeleton" style="height:350px"></div>').join('')} </div> <h3 class="sidebar-title mt-5">📰 ข่าวสารล่าสุด (Latest Updates)</h3> <div id="home-latest-news" class="grid-2"> ${Array(4).fill('<div class="skeleton" style="height:150px"></div>').join('')} </div> </div> <aside class="news-sidebar"> <h3 class="sidebar-title">🔥 ยอดนิยม (Most Read)</h3> <div id="home-trending-news"></div> <div class="card mt-4 funding-card"> <h4>${t('funding_gap')}</h4> <div id="home-funding-gap"></div> <a href="/donate" class="btn btn-donate btn-full mt-3">${t('donate_now')}</a> </div> <div class="card mt-4" style="background:var(--primary); color:#fff; padding:24px; border-radius:var(--radius-lg);"> <h4>ร่วมเป็นอาสาสมัคร</h4> <p style="font-size:0.85rem; opacity:0.8; margin:10px 0 20px;">ใช้ทักษะของคุณเพื่อเปลี่ยนแปลงชีวิตเยาวชนไทย</p> <a href="/volunteer" class="btn btn-accent btn-full">สมัครเลย</a> </div> </aside> </div> </div> </section> `; fetchHomeData(); } async function fetchHomeData() { try { const uProg = `${API_DATA}?endpoint=programs&limit=2`; const uNews = `${API_DATA}?endpoint=news&limit=6`; const uFund = `${API_DATA}?endpoint=funding`; console.log('📡 Fetching Home Data:', { uProg, uNews, uFund }); const [progRes, newsRes, fundRes] = await Promise.all([ fetch(uProg), fetch(uNews), fetch(uFund) ]); if (!progRes.ok || !newsRes.ok || !fundRes.ok) throw new Error('API request failed'); const progs = await progRes.json(); const news = await newsRes.json(); const funds = await fundRes.json(); if (progs.success) { document.getElementById('featured-programs').innerHTML = progs.data.map(p => ` <div class="card reveal"> <div style="position:relative"> <img src="${p.featured_image || 'https://placehold.co/600x400'}" class="card-img" style="height:240px;"> <div class="card-tag" style="position:absolute; top:15px; right:15px;">${p.category || 'Project'}</div> </div> <div class="card-body"> <h3 class="card-title">${state.lang === 'th' ? p.title : (p.title_en || p.title)}</h3> <p class="card-desc">${state.lang === 'th' ? (p.short_desc || '') : (p.short_desc_en || p.short_desc || '')}</p> <a href="/programs/${p.slug}" class="btn btn-outline btn-sm">${t('read_more')}</a> </div> </div> `).join(''); } if (news.success) { const latest = news.data.slice(0, 4); const trending = [...news.data].sort((a,b) => (b.views || 0) - (a.views || 0)).slice(0, 5); document.getElementById('home-latest-news').innerHTML = latest.map(n => ` <div class="card reveal" style="display:flex; gap:15px; padding:12px;"> <img src="${n.featured_image}" style="width:100px; height:100px; object-fit:cover; border-radius:8px; flex-shrink:0;"> <div> <small class="text-muted">${new Date(n.created_at).toLocaleDateString('th-TH')}</small> <h4 style="font-size:0.95rem; margin-top:4px;">${state.lang === 'th' ? n.title : (n.title_en || n.title)}</h4> <a href="/news/${n.slug}" class="text-primary" style="font-size:0.8rem; font-weight:600;">อ่านต่อ ></a> </div> </div> `).join(''); document.getElementById('home-trending-news').innerHTML = trending.map(n => ` <a href="/news/${n.slug}" class="trending-item reveal"> <img src="${n.featured_image}" class="trending-img"> <div class="trending-info"> <h4>${state.lang === 'th' ? n.title : (n.title_en || n.title)}</h4> <div class="meta">👁️ ${n.views || 0} views</div> </div> </a> `).join(''); } if (funds.success) { const goal = funds.data[0]; if (goal) { const percent = Math.min(100, Math.round((goal.current_amount / goal.target_amount) * 100)); document.getElementById('home-funding-gap').innerHTML = ` <div class="mt-2"> <h5 style="color:#fff; font-size:0.9rem;">${state.lang === 'th' ? goal.title : (goal.title_en || goal.title)}</h5> <div class="progress-bar mt-2"> <div class="progress-fill" style="width: ${percent}%"></div> </div> <div class="flex-between mt-1" style="font-size:0.75rem; opacity:0.8; color:#fff;"> <span>${percent}%</span> <span>฿${Number(goal.current_amount).toLocaleString()} / ฿${Number(goal.target_amount).toLocaleString()}</span> </div> </div> `; } } initAnimations(); } catch (err) { console.error('Home data error:', err); } } async function fetchFeaturedPrograms() { const container = document.getElementById('featured-programs'); if (!container) return; try { const res = await fetch(`${API_DATA}?endpoint=programs&limit=3`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (data.success) { container.innerHTML = data.data.map((p, i) => ` <div class="card reveal" style="animation-delay:${i * 0.1}s"> <div style="position:relative"> <img src="${p.featured_image || 'https://placehold.co/400x300?text=No+Image'}" class="card-img" alt="${p.title}"> <div class="card-tag" style="position:absolute; top:10px; right:10px;">${state.lang === 'th' ? (p.category || 'โครงการ') : (p.category_en || p.category || 'Project')}</div> </div> <div class="card-body"> <h3 class="card-title">${state.lang === 'th' ? p.title : (p.title_en || p.title)}</h3> <p class="card-desc">${state.lang === 'th' ? (p.short_desc || '') : (p.short_desc_en || p.short_desc || '')}</p> <a href="/programs/${p.slug}" class="btn btn-outline btn-sm">${t('read_more')}</a> </div> </div> `).join(''); } } catch (err) { console.error('Featured Programs fetch failed:', err); } } async function fetchFundingData() { const container = document.getElementById('home-funding-gap'); if (!container) return; try { const res = await fetch(`${API_DATA}?endpoint=funding`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (data.success && data.data.length > 0) { container.innerHTML = data.data.slice(0, 2).map(goal => { const percent = Math.min(100, Math.round((goal.current_amount / goal.target_amount) * 100)); return ` <div class="funding-card mt-3 reveal"> <h4>${state.lang === 'th' ? escapeHtml(goal.title) : escapeHtml(goal.title_en || goal.title)}</h4> <p>${state.lang === 'th' ? escapeHtml(goal.description) : escapeHtml(goal.description_en || goal.description)}</p> <div class="progress-bar"> <div class="progress-fill" style="width: 0%" data-width="${percent}%"></div> </div> <div class="funding-amounts"> <span>${t('raised')}: <strong>฿${Number(goal.current_amount).toLocaleString()}</strong></span> <span>${t('goal')}: <strong>฿${Number(goal.target_amount).toLocaleString()}</strong></span> </div> </div> `; }).join(''); setTimeout(() => { container.querySelectorAll('.progress-fill').forEach(fill => { fill.style.width = fill.dataset.width; }); }, 500); } } catch(err) { console.error('Funding fetch error:', err); } } async function renderAbout(app) { app.innerHTML = ` <div class="page-hero"> <div class="container text-center"> <h1 class="animate-up">${t('about')}</h1> <p class="animate-up" style="animation-delay:0.1s">${state.lang === 'th' ? 'มารู้จักกับมูลนิธิส่งเสริมเยาวชนไทย' : 'Get to know Thai Youth Foundation'}</p> </div> </div> <div class="container section"> <div class="card card-body reveal"> <h2 class="text-primary mb-4">${state.lang === 'th' ? 'ประวัติความเป็นมา' : 'Our History'}</h2> <p>${state.lang === 'th' ? 'มูลนิธิส่งเสริมเยาวชนไทยก่อตั้งขึ้นเมื่อปี พ.ศ. 2550...' : 'Thai Youth Foundation was founded in 2007...'}</p> <p class="mt-3">${state.lang === 'th' ? 'วิสัยทัศน์ของเราคือ "เยาวชนทุกคนต้องได้รับโอกาสที่เท่าเทียมในการพัฒนาตนเอง"' : 'Our vision is "Every youth must receive equal opportunity for self-development"'}</p> </div> </div> `; } async function renderPrograms(app) { app.innerHTML = ` <div class="page-hero editorial-hero"> <div class="container text-center"> <div class="ticker-label mx-auto mb-3" style="display:inline-block">Impact</div> <h1 class="animate-up serif" style="font-size:3rem;">โครงการพัฒนาศักยภาพเยาวชน</h1> <p class="animate-up" style="animation-delay:0.1s; max-width:600px; margin:0 auto;">ร่วมสร้างโอกาสที่ยั่งยืนผ่านโครงการที่ครอบคลุมทุกด้านของความต้องการพื้นฐานและการศึกษา</p> </div> </div> <div class="container section"> <div class="flex-between mb-4"> <div class="category-tabs"> <button class="cat-tab active" onclick="filterPrograms('all')">ทั้งหมด</button> <button class="cat-tab" onclick="filterPrograms('education')">การศึกษา</button> <button class="cat-tab" onclick="filterPrograms('community')">ชุมชน</button> <button class="cat-tab" onclick="filterPrograms('leadership')">ความเป็นผู้นำ</button> <button class="cat-tab" onclick="filterPrograms('creative')">งานสร้างสรรค์</button> </div> </div> <div id="programs-grid" class="grid-3"> ${Array(6).fill('<div class="skeleton" style="height:350px"></div>').join('')} </div> </div> `; fetchProgramsData(); } async function fetchProgramsData(category = 'all') { const grid = document.getElementById('programs-grid'); if (!grid) return; console.log(`[Fetch] Loading programs for category: ${category}...`); try { // Add cache busting to ensure we get fresh data const t = new Date().getTime(); const res = await fetch(`${API_DATA}?endpoint=programs&category=${category}&_t=${t}`, { cache: 'no-cache' }); if (!res.ok) throw new Error(`Server returned ${res.status} ${res.statusText}`); const text = await res.text(); let data; try { data = JSON.parse(text); } catch (e) { console.error('Invalid JSON response:', text); throw new Error('Server returned invalid data format'); } if (data.success) { let items = data.data || []; if (items.length === 0) { grid.innerHTML = ` <div class="col-span-full text-center py-5"> <p class="text-muted">${state.lang === 'th' ? 'ไม่พบข้อมูลโครงการในหมวดหมู่นี้' : 'No programs found in this category'}</p> </div>`; return; } grid.innerHTML = items.map(p => ` <div class="card reveal editorial-card"> <div class="card-img-wrapper"> <img src="${p.featured_image || 'https://placehold.co/400x300?text=No+Image'}" class="card-img" alt="${p.title}" onerror="this.src='https://placehold.co/400x300?text=Image+Error'"> <div class="news-tag">${state.lang === 'th' ? (p.category || 'โครงการ') : (p.category_en || p.category || 'Project')}</div> </div> <div class="card-body"> <h3 class="card-title serif">${state.lang === 'th' ? p.title : (p.title_en || p.title)}</h3> <p class="card-desc">${state.lang === 'th' ? (p.short_desc || '') : (p.short_desc_en || p.short_desc || '')}</p> <div class="card-footer mt-auto"> <a href="/programs/${p.slug}" class="text-primary font-bold">${state.lang === 'th' ? 'อ่านรายละเอียด →' : 'View Details →'}</a> </div> </div> </div> `).join(''); initAnimations(); } else { throw new Error(data.message || data.debug || 'Data loading failed'); } } catch (err) { console.error('Programs data error:', err); // Added alert to help user see error on live site if (!window.location.origin.includes('localhost')) { alert('Data Load Error: ' + err.message + '\nPlease check ' + API_BASE + '/debug_db.php'); } grid.innerHTML = ` <div class="col-span-full text-center py-5"> <div class="card card-body" style="border-top:3px solid var(--red); max-width:500px; margin:0 auto;"> <h3 class="text-primary mb-3">⚠️ ขออภัย</h3> <p class="text-muted">เกิดข้อผิดพลาดในการโหลดข้อมูลโครงการ</p> <div class="p-2 mt-3 mb-3 bg-light rounded text-xs font-mono" style="font-size:0.8rem; background:rgba(0,0,0,0.05);">${err.message}</div> <button class="btn btn-primary btn-sm mx-auto" onclick="fetchProgramsData('${category}')">ลองใหม่อีกครั้ง</button> </div> </div>`; } } window.filterPrograms = (cat) => { document.querySelectorAll('.cat-tab').forEach(t => t.classList.remove('active')); event.target.classList.add('active'); fetchProgramsData(cat); }; async function renderProgramDetail(app, slug) { const res = await fetch(`${API_DATA}?endpoint=programs&slug=${slug}`); const data = await res.json(); if (!data.success) return renderNotFound(app); const p = data.data; app.innerHTML = ` <div class="page-hero"> <div class="container"> <div class="breadcrumb"> <a href="/">${t('home')}</a> <span class="breadcrumb-sep">></span> <a href="/programs">${t('programs')}</a> <span class="breadcrumb-sep">></span> <span>${state.lang === 'th' ? p.title : (p.title_en || p.title)}</span> </div> <h1 class="animate-up">${state.lang === 'th' ? p.title : (p.title_en || p.title)}</h1> </div> </div> <div class="container section"> <div class="grid-2"> <div class="reveal"> <img src="${p.featured_image || 'https://placehold.co/800x600?text=No+Image'}" style="border-radius:16px; box-shadow:var(--shadow-lg);"> </div> <div class="reveal" style="animation-delay:0.2s"> <div class="cms-content"> ${state.lang === 'th' ? (p.content || p.short_desc) : (p.content_en || p.short_desc_en || p.short_desc)} </div> <div class="mt-4 p-4" style="background:#EFF4FF; border-radius:12px;"> <h3 class="text-primary mb-2">${state.lang === 'th' ? 'สนใจร่วมสนับสนุน?' : 'Interested in Supporting?'}</h3> <p class="mb-3">${state.lang === 'th' ? 'คุณสามารถร่วมเป็นส่วนหนึ่งของโครงการนี้ผ่านการบริจาคหรือสมัครเป็นอาสาสมัคร' : 'You can be part of this project through donation or volunteering.'}</p> <div class="flex gap-2"> <a href="/donate" class="btn btn-donate">${t('donate')}</a> <a href="/volunteer" class="btn btn-outline">${t('volunteer')}</a> </div> </div> </div> </div> </div> `; } async function renderNews(app) { app.innerHTML = ` <div class="page-hero"> <div class="container"> <h1 class="animate-up">ข่าวสารและกิจกรรม</h1> <p class="animate-up" style="animation-delay:0.1s">เกาะติดสถานการณ์และข่าวสารสำคัญเพื่อเยาวชนไทย</p> </div> </div> <div class="container section"> <div id="news-portal-loader" class="text-center"> <div class="spinner" style="margin: 0 auto;"></div> <p class="mt-3">กำลังรวบรวมข่าวสาร...</p> </div> <div id="news-portal-content" class="news-portal-grid" style="display:none;"> <div class="news-main"> <div id="news-hero-section"></div> <h3 class="sidebar-title">📰 ข่าวล่าสุด (Latest News)</h3> <div id="news-latest-grid" class="grid-2"></div> </div> <aside class="news-sidebar"> <h3 class="sidebar-title">🔥 ข่าวยอดนิยม (Most Read)</h3> <div id="news-trending-list"></div> <div class="card mt-4" style="background:var(--primary); color:#fff; padding:24px; border:none;"> <h4>สมัครรับข่าวสาร</h4> <p style="font-size:0.85rem; opacity:0.8; margin:10px 0 20px;">ไม่พลาดทุกกิจกรรมและโครงการใหม่ๆ จากทางมูลนิธิฯ</p> <div class="flex gap-2"> <input type="email" placeholder="อีเมลของคุณ" style="padding:10px; border-radius:6px; border:none; flex:1;"> <button class="btn btn-accent btn-sm" style="background:var(--accent); color:var(--text);">ตกลง</button> </div> </div> </aside> </div> </div> `; try { const res = await fetch(`${API_DATA}?endpoint=news`); const data = await res.json(); if (data.success && data.data.length > 0) { const news = data.data; const heroNews = news[0]; const latestNews = news.slice(1, 7); // Next 6 news const trendingNews = [...news].sort((a, b) => (b.views || 0) - (a.views || 0)).slice(0, 5); // Sort by views // 1. Render Hero Slider (Featured) const heroContainer = document.getElementById('news-hero-section'); heroContainer.innerHTML = ` <a href="/news/${heroNews.slug}" class="news-hero-slider reveal"> <img src="${heroNews.featured_image || 'https://placehold.co/1200x600?text=News'}" class="news-hero-img"> <div class="news-hero-content"> <span class="news-tag">Breaking News</span> <h2>${state.lang === 'th' ? heroNews.title : (heroNews.title_en || heroNews.title)}</h2> <div class="meta"> <span>📅 ${new Date(heroNews.created_at).toLocaleDateString('th-TH')}</span> <span>👁️ ${heroNews.views || 0} views</span> </div> </div> </a> `; // 2. Render Latest Grid const latestGrid = document.getElementById('news-latest-grid'); latestGrid.innerHTML = latestNews.map(n => ` <div class="card reveal"> <img src="${n.featured_image}" class="card-img" style="height:180px;"> <div class="card-body"> <small class="text-muted">${new Date(n.created_at).toLocaleDateString('th-TH')}</small> <h3 class="card-title mt-2" style="font-size:1rem;">${state.lang === 'th' ? n.title : (n.title_en || n.title)}</h3> <p class="card-desc" style="font-size:0.85rem;">${state.lang === 'th' ? (n.excerpt || '') : (n.excerpt_en || n.excerpt || '')}</p> <a href="/news/${n.slug}" class="btn btn-outline btn-sm">${t('read_more')}</a> </div> </div> `).join(''); // 3. Render Trending Sidebar const trendingList = document.getElementById('news-trending-list'); trendingList.innerHTML = trendingNews.map(n => ` <a href="/news/${n.slug}" class="trending-item reveal"> <img src="${n.featured_image}" class="trending-img"> <div class="trending-info"> <h4>${state.lang === 'th' ? n.title : (n.title_en || n.title)}</h4> <div class="meta">👁️ ${n.views || 0} views</div> </div> </a> `).join(''); // Show content document.getElementById('news-portal-loader').style.display = 'none'; document.getElementById('news-portal-content').style.display = 'grid'; initAnimations(); } else { document.getElementById('news-portal-loader').innerHTML = '<p>ไม่พบข้อมูลข่าวสารในขณะนี้</p>'; } } catch (err) { console.error('News Portal Error:', err); document.getElementById('news-portal-loader').innerHTML = '<p class="text-danger">เกิดข้อผิดพลาดในการโหลดข้อมูล</p>'; } } async function renderNewsDetail(app, slug) { if (!slug) return renderNotFound(app); const res = await fetch(`${API_DATA}?endpoint=news&slug=${slug}`); const data = await res.json(); // Safety check: Ensure data is an object, not an array or null if (!data.success || Array.isArray(data.data) || !data.data) return renderNotFound(app); const n = data.data; const title = state.lang === 'th' ? (n.title || '') : (n.title_en || n.title || ''); const content = state.lang === 'th' ? (n.content || n.excerpt || '') : (n.content_en || n.excerpt_en || n.content || n.excerpt || ''); app.innerHTML = ` <div class="page-hero"> <div class="container"> <div class="breadcrumb"> <a href="/">${t('home')}</a> <span class="breadcrumb-sep">></span> <a href="/news">${t('news')}</a> <span class="breadcrumb-sep">></span> <span>${escapeHtml(title)}</span> </div> <h1 class="animate-up">${escapeHtml(title)}</h1> <p class="mt-2 text-muted animate-up" style="animation-delay:0.1s"> 📅 ${new Date(n.created_at).toLocaleDateString(state.lang === 'th' ? 'th-TH' : 'en-US')} | 👁️ ${n.views || 0} views </p> </div> </div> <div class="container section"> <div class="reveal"> <img src="${n.featured_image || 'https://placehold.co/1200x600?text=News'}" style="width:100%; max-height:500px; object-fit:cover; border-radius:16px; box-shadow:var(--shadow-lg); margin-bottom:40px;"> <div class="cms-content card card-body"> ${content} </div> </div> </div> `; } async function renderGallery(app) { // 1. Initial UI Structure app.innerHTML = ` <div class="page-hero" style="background: linear-gradient(135deg, var(--primary), var(--primary-dark)); padding: 80px 0;"> <div class="container text-center"> <span class="ticker-label mx-auto mb-3" style="background:rgba(255,255,255,0.2); color:#fff; border:1px solid rgba(255,255,255,0.3);">OUR MEMORIES</span> <h1 class="animate-up serif" style="font-size:3.5rem; color:#fff;">คลังภาพกิจกรรม</h1> <p class="animate-up" style="opacity:0.8; color:#fff; max-width:600px; margin:0 auto;">บันทึกความประทับใจและการเปลี่ยนแปลงที่เราสร้างร่วมกันผ่านเลนส์</p> </div> </div> <div class="container section"> <div class="gallery-filters mb-5 reveal"> <div class="category-tabs" id="gallery-albums" style="justify-content:center; flex-wrap:wrap;"> <button class="cat-tab active" data-album="all">ทั้งหมด</button> </div> </div> <div id="gallery-timeline-content" class="timeline-wrapper"> <div class="text-center py-5"> <div class="spinner mx-auto"></div> <p class="mt-3">กำลังจัดระเบียบความทรงจำ...</p> </div> </div> </div> `; try { const res = await fetch(`${API_DATA}?endpoint=gallery`); const data = await res.json(); if (data.success && data.data.length > 0) { const galleryData = data.data; // Extract Unique Albums const albums = ['all', ...new Set(galleryData.map(img => img.album || 'ทั่วไป'))]; const albumContainer = document.getElementById('gallery-albums'); albumContainer.innerHTML = albums.map(alb => ` <button class="cat-tab ${alb === 'all' ? 'active' : ''}" data-album="${alb}">${alb === 'all' ? 'ทั้งหมด' : alb}</button> `).join(''); const renderTimeline = (filterAlbum = 'all') => { const filtered = filterAlbum === 'all' ? galleryData : galleryData.filter(img => (img.album || 'ทั่วไป') === filterAlbum); const groups = {}; filtered.forEach(img => { const date = new Date(img.created_at); const monthKey = date.toLocaleDateString('th-TH', { month: 'long', year: 'numeric' }); if (!groups[monthKey]) groups[monthKey] = []; groups[monthKey].push(img); }); const timelineContent = document.getElementById('gallery-timeline-content'); if (filtered.length === 0) { timelineContent.innerHTML = `<div class="text-center py-5"><p>ไม่พบรูปภาพในหมวดหมู่นี้</p></div>`; return; } timelineContent.innerHTML = Object.keys(groups).map(month => ` <div class="timeline-section mb-5 reveal"> <div class="timeline-header mb-4" style="display:flex; align-items:center; gap:20px;"> <h2 class="serif" style="color:var(--primary); font-size:1.8rem; white-space:nowrap;">${month}</h2> <div style="height:2px; background:linear-gradient(to right, var(--border), transparent); width:100%;"></div> </div> <div class="gallery-grid"> ${groups[month].map(img => ` <div class="gallery-item reveal" onclick="openGalleryLightbox('${img.file_path}', '${escapeHtml(img.title)}')"> <img src="${img.file_path}" alt="${img.title}" loading="lazy" style="width:100%; height:100%; object-fit:cover;"> <div class="gallery-item-overlay"> <span class="gallery-tag">${img.album || 'ทั่วไป'}</span> <h4>${escapeHtml(img.title)}</h4> <small>📅 ${new Date(img.created_at).toLocaleDateString('th-TH')}</small> </div> </div> `).join('')} </div> </div> `).join(''); if (typeof initAnimations === 'function') initAnimations(); }; renderTimeline(); albumContainer.addEventListener('click', (e) => { if (e.target.classList.contains('cat-tab')) { document.querySelectorAll('.cat-tab').forEach(t => t.classList.remove('active')); e.target.classList.add('active'); renderTimeline(e.target.dataset.album); } }); } else { document.getElementById('gallery-timeline-content').innerHTML = `<div class="text-center py-5"><h3 class="text-muted">ยังไม่มีรูปภาพในแกลเลอรี</h3></div>`; } } catch (err) { console.error('Gallery Render Error:', err); document.getElementById('gallery-timeline-content').innerHTML = `<p class="text-center text-danger">เกิดข้อผิดพลาดในการโหลดข้อมูล</p>`; } } window.openGalleryLightbox = (src, title) => { const lightbox = document.createElement('div'); lightbox.className = 'lightbox-overlay'; lightbox.style = ` position: fixed; inset: 0; background: rgba(0,0,0,0.95); backdrop-filter: blur(15px); z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); `; lightbox.innerHTML = ` <div style="position:absolute; top:30px; right:30px; color:#fff; font-size:3rem; line-height:1;">×</div> <img src="${src}" style="max-width:90%; max-height:80vh; object-fit:contain; border-radius:12px; box-shadow:0 30px 60px rgba(0,0,0,0.8); transform: scale(0.9); animation: zoomIn 0.3s forwards 0.1s;"> <h3 style="color:#fff; margin-top:30px; font-family:var(--font-th); font-size:1.5rem; text-shadow:0 2px 4px rgba(0,0,0,0.5);">${title}</h3> `; lightbox.onclick = () => { lightbox.style.opacity = '0'; setTimeout(() => lightbox.remove(), 400); }; document.body.appendChild(lightbox); }; function escapeHtml(unsafe) { if(!unsafe) return ''; return String(unsafe).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); } async function renderCMSPage(app, page) { app.innerHTML = ` <div class="page-hero"> <div class="container text-center"> <h1 class="animate-up">${state.lang === 'th' ? page.title : (page.title_en || page.title)}</h1> </div> </div> <div class="container section"> <div class="card card-body reveal cms-content"> ${state.lang === 'th' ? page.content : (page.content_en || page.content)} </div> </div> `; } async function renderVolunteer(app) { app.innerHTML = ` <div class="page-hero"><div class="container text-center"><h1>สมัครอาสาสมัคร</h1></div></div> <div class="container section"> <div class="card form-card reveal" style="max-width:600px; margin:0 auto;"> <form id="volunteer-form"> <div class="form-group"><label>ชื่อ-นามสกุล <span class="required">*</span></label><input type="text" id="vol-name" required></div> <div class="form-row"> <div class="form-group"><label>อีเมล <span class="required">*</span></label><input type="email" id="vol-email" required></div> <div class="form-group"><label>เบอร์โทรศัพท์ <span class="required">*</span></label><input type="tel" id="vol-phone" required></div> </div> <div class="form-group"><label>ทักษะ/ความถนัด</label><textarea id="vol-skills"></textarea></div> <button type="submit" class="btn btn-primary btn-full">ส่งใบสมัคร</button> </form> </div> </div> `; document.getElementById('volunteer-form').onsubmit = async (e) => { e.preventDefault(); const payload = { name: document.getElementById('vol-name').value, email: document.getElementById('vol-email').value, phone: document.getElementById('vol-phone').value, skills: document.getElementById('vol-skills').value }; const res = await fetch(`${API_DATA}?endpoint=volunteers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if(data.success) { showToast('สมัครสำเร็จ ขอบคุณครับ', 'success'); e.target.reset(); } }; } async function renderContact(app) { app.innerHTML = ` <div class="page-hero"><div class="container text-center"><h1>ติดต่อเรา</h1></div></div> <div class="container section"> <div class="contact-grid"> <div class="reveal"> <h2>มูลนิธิส่งเสริมเยาวชนไทย</h2> <div class="contact-item mt-4">📍 123 ถนนพระราม 4 กรุงเทพฯ 10330</div> <div class="contact-item">📞 +66 2 123 4567</div> <div class="contact-item">📧 info@thaiyouthfoundation.org</div> </div> <div class="card form-card reveal"> <form id="contact-form"> <div class="form-group"><label>ชื่อ</label><input type="text" id="c-name" required></div> <div class="form-group"><label>อีเมล</label><input type="email" id="c-email" required></div> <div class="form-group"><label>ข้อความ</label><textarea id="c-msg" required></textarea></div> <button type="submit" class="btn btn-primary btn-full">ส่งข้อความ</button> </form> </div> </div> </div> `; document.getElementById('contact-form').onsubmit = async (e) => { e.preventDefault(); const payload = { name: document.getElementById('c-name').value, email: document.getElementById('c-email').value, subject: 'Contact from Web', message: document.getElementById('c-msg').value }; const res = await fetch(`${API_DATA}?endpoint=contacts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if(data.success) { showToast('ส่งข้อความแล้ว ทีมงานจะรีบติดต่อกลับครับ', 'success'); e.target.reset(); } }; } async function renderDonate(app) { app.innerHTML = ` <div class="page-hero"><div class="container text-center"><h1>ร่วมบริจาค</h1></div></div> <div class="container section"> <div class="card form-card reveal" style="max-width:600px; margin:0 auto;"> <form id="donate-form"> <h3 class="mb-3">ระบุจำนวนเงิน</h3> <input type="number" id="d-amount" class="form-group" style="font-size:2rem; text-align:center;" value="500" min="1"> <div class="form-group"><label>ชื่อผู้บริจาค</label><input type="text" id="d-name" required value="${state.user ? state.user.name : ''}"></div> <div class="form-group"><label>อีเมล</label><input type="email" id="d-email" required value="${state.user ? state.user.email : ''}"></div> <button type="submit" class="btn btn-donate btn-full">ยืนยันการบริจาค</button> </form> </div> </div> `; document.getElementById('donate-form').onsubmit = async (e) => { e.preventDefault(); const payload = { amount: document.getElementById('d-amount').value, name: document.getElementById('d-name').value, email: document.getElementById('d-email').value, payment_method: 'bank_transfer', status: 'pending' }; const res = await fetch(`${API_DATA}?endpoint=donations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if(data.success) { showToast('ขอบคุณสำหรับการบริจาคครับ', 'success'); setTimeout(()=>navigateTo('/'), 2000); } }; } function renderNotFound(app) { app.innerHTML = `<div class="container section text-center"><h1>404</h1><p>ไม่พบหน้าที่ต้องการ</p><a href="/" class="btn btn-primary mt-3">กลับหน้าแรก</a></div>`; } // ========================================== // AUTH & UI // ========================================== function initUI() { const hamburger = document.getElementById('hamburger'); const nav = document.getElementById('main-nav'); if (hamburger) hamburger.onclick = () => nav.classList.toggle('open'); const logoutBtn = document.getElementById('logout-btn'); if(logoutBtn) logoutBtn.onclick = (e) => { e.preventDefault(); logout(); }; // Handle Search Interaction (Thairath Style) const searchTrigger = document.querySelector('.search-trigger'); if (searchTrigger) { searchTrigger.onclick = () => { const query = prompt(state.lang === 'th' ? 'ค้นหาเรื่องที่คุณสนใจ...' : 'Search for what you are interested in...'); if (query && query.trim()) { window.location.href = `/search?q=${encodeURIComponent(query.trim())}`; } }; } } async function fetchUser() { try { const res = await fetch(`${API_AUTH}?action=me`, { headers: { 'Authorization': `Bearer ${state.token}` } }); const data = await res.json(); if (data.success) { state.user = data.user; updateAuthUI(); } else logout(); } catch { logout(); } } function updateAuthUI() { const loginLink = document.getElementById('login-link'); const userMenu = document.getElementById('user-menu'); const userName = document.getElementById('user-name-display'); if (state.user) { if(loginLink) loginLink.style.display = 'none'; if(userMenu) userMenu.style.display = 'block'; if(userName) userName.textContent = state.user.name.split(' ')[0]; if(state.user.role === 'admin') { const drop = document.getElementById('user-dropdown'); if(!drop.querySelector('.admin-link')) { const a = document.createElement('a'); a.href = '/admin/'; a.className='admin-link'; a.textContent='แผงควบคุม (Admin)'; drop.insertBefore(a, drop.firstChild); } } } else { if(loginLink) loginLink.style.display = 'inline-flex'; if(userMenu) userMenu.style.display = 'none'; } } function logout() { state.user = null; state.token = null; localStorage.removeItem('token'); updateAuthUI(); showToast('ออกจากระบบแล้ว'); navigateTo('/'); } // MODAL window.openModal = (type) => { document.getElementById('auth-modal').style.display = 'flex'; switchTab(type || 'login'); }; window.closeModal = () => { document.getElementById('auth-modal').style.display = 'none'; }; window.switchTab = (tab) => { const isLogin = tab === 'login'; document.getElementById('login-form-container').style.display = isLogin ? 'block' : 'none'; document.getElementById('register-form-container').style.display = isLogin ? 'none' : 'block'; // Update active tab styling document.querySelectorAll('.auth-tab').forEach(btn => { const isMatch = (isLogin && btn.textContent.includes('เข้าสู่ระบบ')) || (!isLogin && btn.textContent.includes('ลงทะเบียน')); btn.classList.toggle('active', isMatch); }); }; window.handleLogin = async (e) => { e.preventDefault(); const payload = { email: document.getElementById('login-email').value, password: document.getElementById('login-password').value }; const res = await fetch(`${API_AUTH}?action=login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if(data.success) { state.token = data.token; state.user = data.user; localStorage.setItem('token', data.token); updateAuthUI(); closeModal(); showToast('เข้าสู่ระบบสำเร็จ'); } else { document.getElementById('login-error').textContent = data.message; document.getElementById('login-error').style.display = 'block'; } }; window.handleRegister = async (e) => { e.preventDefault(); const payload = { name: document.getElementById('reg-name').value, email: document.getElementById('reg-email').value, phone: document.getElementById('reg-phone').value, password: document.getElementById('reg-password').value }; const res = await fetch(`${API_AUTH}?action=register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if(data.success) { state.token = data.token; state.user = data.user; localStorage.setItem('token', data.token); updateAuthUI(); closeModal(); showToast('ลงทะเบียนสำเร็จ'); } else { document.getElementById('reg-error').textContent = data.message; document.getElementById('reg-error').style.display = 'block'; } }; // UTILS & ANIMATIONS function initAnimations() { const obs = new IntersectionObserver((es) => es.forEach(e => { if(e.isIntersecting) e.target.classList.add('active'); }), { threshold: 0.1 }); document.querySelectorAll('.reveal').forEach(el => obs.observe(el)); } function initCounterAnimations() { document.querySelectorAll('.number').forEach(c => { const t = +c.dataset.target; const u = () => { const n = +c.innerText.replace(/[^0-9]/g, ''); const i = Math.ceil(t/100); if(n<t) { c.innerText = (n+i).toLocaleString()+'+'; setTimeout(u, 20); } else c.innerText = t.toLocaleString()+'+'; }; u(); }); } function updateGlobalUI() { if(state.settings.site_name) { document.title = state.settings.site_name; document.querySelectorAll('.site-name').forEach(e => e.textContent = state.settings.site_name); } } window.showToast = (msg, type = 'success') => { const t = document.getElementById('toast'); t.textContent = msg; t.className = `toast ${type}`; t.style.display = 'block'; setTimeout(() => t.style.display = 'none', 3000); }; function escapeHtml(u) { if(!u) return ''; return String(u).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); }
💾 Simpan File
Batal
⬅ Naik ke public_html
2 item
Nama
Tipe
Ukuran
Diubah
Aksi
📜
app.js
js
56.4 KB
2026-05-06 10:30
✏️ Edit
👁️ View
🗑 Hapus
📜
app_v2.js
js
73.4 KB
2026-05-08 04:46
✏️ Edit
👁️ View
🗑 Hapus