Major changes: - Replace Payload CMS with Tina CMS (self-hosted) - Add Astro DB for consent logging (PDPA compliant) - Update Tailwind v3 to v4 (@tailwindcss/vite plugin) - Add astro-tina-starter template - Rewrite consent template for Astro (ConsentBanner.astro, Astro DB, Nano Stores) - Add install-tina-backend.sh for self-hosted Tina per customer - Rename convert-astro.sh to migrate-tina.sh - Add AGENTS.md template for generated websites - Delete all Payload/Next.js files Technical updates: - Astro DB using defineDb with eq operators for queries - Tailwind v4 with @theme block - Tina CMS local development mode - Proper Astro API routes for consent Research-verified with official documentation (April 2026)
447 lines
12 KiB
Plaintext
447 lines
12 KiB
Plaintext
---
|
|
/**
|
|
* PDPA Consent Banner Component for Astro + Tina
|
|
* Replaces cookie-banner.tsx from Next.js+Payload
|
|
*
|
|
* Usage: Import and add <ConsentBanner /> to your layout
|
|
*/
|
|
|
|
interface Props {
|
|
/** Optional: Custom privacy policy URL */
|
|
privacyPolicyUrl?: string;
|
|
}
|
|
|
|
const { privacyPolicyUrl = "/privacy-policy" } = Astro.props;
|
|
---
|
|
|
|
<div
|
|
id="pdpa-consent-banner"
|
|
class="consent-banner"
|
|
role="dialog"
|
|
aria-label="Cookie Consent Banner"
|
|
aria-hidden="true"
|
|
>
|
|
<div class="consent-banner__content">
|
|
<!-- Main Banner -->
|
|
<div id="consent-main" class="consent-banner__main">
|
|
<h3 class="consent-banner__title">
|
|
🍪 การยินยอมตาม พ.ร.บ.คุ้มครองข้อมูลส่วนบุคคล
|
|
</h3>
|
|
<p class="consent-banner__text">
|
|
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งานเว็บไซต์ของคุณ การเข้าชมเว็บไซต์ต่อถือว่าคุณยินยอมให้เราใช้คุกกี้{' '}
|
|
<a href={privacyPolicyUrl} class="consent-banner__link">เรียนรู้เพิ่มเติม</a>
|
|
</p>
|
|
|
|
<div class="consent-banner__buttons">
|
|
<button
|
|
id="consent-accept-all"
|
|
class="consent-btn consent-btn--accept"
|
|
type="button"
|
|
>
|
|
ยอมรับทั้งหมด
|
|
</button>
|
|
|
|
<button
|
|
id="consent-reject-all"
|
|
class="consent-btn consent-btn--reject"
|
|
type="button"
|
|
>
|
|
ปฏิเสธทั้งหมด
|
|
</button>
|
|
|
|
<button
|
|
id="consent-show-preferences"
|
|
class="consent-btn consent-btn--preferences"
|
|
type="button"
|
|
>
|
|
ตั้งค่าคุกกี้
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preferences Panel -->
|
|
<div id="consent-preferences" class="consent-banner__preferences" style="display: none;">
|
|
<h3 class="consent-banner__title">ตั้งค่าคุกกี้</h3>
|
|
|
|
<p class="consent-banner__text" style="margin-bottom: 1rem; color: #555; font-size: 0.875rem;">
|
|
จัดการการตั้งค่าคุกกี้ของคุณด้านล่าง
|
|
</p>
|
|
|
|
<div class="consent-banner__options">
|
|
<!-- Functional Cookies -->
|
|
<div class="consent-option consent-option--disabled">
|
|
<div class="consent-option__header">
|
|
<div>
|
|
<h4 class="consent-option__title">คุกกี้ที่จำเป็น</h4>
|
|
<p class="consent-option__desc">
|
|
จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารปิดได้
|
|
</p>
|
|
</div>
|
|
<span class="consent-option__badge">เปิดอยู่เสมอ</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analytics Cookies -->
|
|
<div class="consent-option">
|
|
<div class="consent-option__header">
|
|
<div>
|
|
<h4 class="consent-option__title">คุกกี้วิเคราะห์</h4>
|
|
<p class="consent-option__desc">
|
|
ช่วยเราเข้าใจว่าผู้เยี่ยมชมใช้งานเว็บไซต์ของเราอย่างไร
|
|
</p>
|
|
</div>
|
|
<label class="consent-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
id="consent-analytics"
|
|
name="analytics"
|
|
class="consent-checkbox__input"
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Marketing Cookies -->
|
|
<div class="consent-option">
|
|
<div class="consent-option__header">
|
|
<div>
|
|
<h4 class="consent-option__title">คุกกี้การตลาด</h4>
|
|
<p class="consent-option__desc">
|
|
ใช้ติดตามผู้เยี่ยมชมข้ามเว็บไซต์เพื่อการโฆษณา
|
|
</p>
|
|
</div>
|
|
<label class="consent-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
id="consent-marketing"
|
|
name="marketing"
|
|
class="consent-checkbox__input"
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="consent-banner__buttons">
|
|
<button
|
|
id="consent-save-preferences"
|
|
class="consent-btn consent-btn--save"
|
|
type="button"
|
|
>
|
|
บันทึกการตั้งค่า
|
|
</button>
|
|
|
|
<button
|
|
id="consent-back"
|
|
class="consent-btn consent-btn--back"
|
|
type="button"
|
|
>
|
|
กลับ
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.consent-banner {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #ffffff;
|
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
|
padding: 1.5rem;
|
|
z-index: 9999;
|
|
border-top: 1px solid #e5e5e5;
|
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
.consent-banner__content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.consent-banner__title {
|
|
margin: 0 0 0.75rem 0;
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.consent-banner__text {
|
|
margin: 0 0 1rem 0;
|
|
color: #555;
|
|
font-size: 0.9375rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.consent-banner__link {
|
|
color: #0066cc;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.consent-banner__link:hover {
|
|
color: #004499;
|
|
}
|
|
|
|
.consent-banner__buttons {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.consent-btn {
|
|
padding: 0.625rem 1.25rem;
|
|
border-radius: 6px;
|
|
font-size: 0.9375rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.consent-btn--accept {
|
|
background-color: #22c55e;
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
.consent-btn--accept:hover {
|
|
background-color: #16a34a;
|
|
}
|
|
|
|
.consent-btn--reject {
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
border: 1px solid #ddd;
|
|
}
|
|
|
|
.consent-btn--reject:hover {
|
|
background-color: #e5e5e5;
|
|
}
|
|
|
|
.consent-btn--preferences {
|
|
background-color: transparent;
|
|
color: #0066cc;
|
|
border: 1px solid #0066cc;
|
|
}
|
|
|
|
.consent-btn--preferences:hover {
|
|
background-color: #f0f9ff;
|
|
}
|
|
|
|
.consent-btn--save {
|
|
background-color: #0066cc;
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
.consent-btn--save:hover {
|
|
background-color: #004499;
|
|
}
|
|
|
|
.consent-btn--back {
|
|
background-color: transparent;
|
|
color: #666;
|
|
border: none;
|
|
}
|
|
|
|
.consent-btn--back:hover {
|
|
color: #333;
|
|
}
|
|
|
|
/* Preferences Panel */
|
|
.consent-banner__options {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.consent-option {
|
|
padding: 1rem;
|
|
background-color: #fff;
|
|
border-radius: 8px;
|
|
margin-bottom: 0.75rem;
|
|
border: 1px solid #e5e5e5;
|
|
}
|
|
|
|
.consent-option--disabled {
|
|
background-color: #f9f9f9;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.consent-option__header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.consent-option__title {
|
|
margin: 0;
|
|
font-size: 0.9375rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.consent-option__desc {
|
|
margin: 0.25rem 0 0 0;
|
|
font-size: 0.8125rem;
|
|
color: #666;
|
|
}
|
|
|
|
.consent-option__badge {
|
|
padding: 0.25rem 0.75rem;
|
|
background-color: #e5e5e5;
|
|
color: #666;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.consent-checkbox__input {
|
|
width: 18px;
|
|
height: 18px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Hide initially via JS */
|
|
.consent-banner[hidden] {
|
|
display: none;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import { consentStore, type ConsentState } from './stores/consent';
|
|
|
|
// DOM Elements
|
|
const banner = document.getElementById('pdpa-consent-banner');
|
|
const mainPanel = document.getElementById('consent-main');
|
|
const prefsPanel = document.getElementById('consent-preferences');
|
|
const analyticsCheckbox = document.getElementById('consent-analytics') as HTMLInputElement;
|
|
const marketingCheckbox = document.getElementById('consent-marketing') as HTMLInputElement;
|
|
|
|
// Button handlers
|
|
const acceptAllBtn = document.getElementById('consent-accept-all');
|
|
const rejectAllBtn = document.getElementById('consent-reject-all');
|
|
const showPrefsBtn = document.getElementById('consent-show-preferences');
|
|
const savePrefsBtn = document.getElementById('consent-save-preferences');
|
|
const backBtn = document.getElementById('consent-back');
|
|
|
|
// Default consent state
|
|
const defaultConsent: ConsentState = {
|
|
analytics: false,
|
|
marketing: false,
|
|
functional: false,
|
|
hasConsented: false,
|
|
};
|
|
|
|
const STORAGE_KEY = 'pdpa_consent';
|
|
|
|
// Save consent to localStorage and server
|
|
async function saveConsent(newConsent: ConsentState) {
|
|
// Save to localStorage
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newConsent));
|
|
|
|
// Update nanostore
|
|
consentStore.set(newConsent);
|
|
|
|
// Hide banner
|
|
if (banner) {
|
|
banner.setAttribute('hidden', 'true');
|
|
}
|
|
|
|
// Log to server
|
|
try {
|
|
await fetch('/api/consent', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: newConsent.hasConsented ? 'accept' : 'reject',
|
|
purpose: 'all',
|
|
analytics: newConsent.analytics,
|
|
marketing: newConsent.marketing,
|
|
functional: newConsent.functional,
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to log consent:', error);
|
|
}
|
|
}
|
|
|
|
// Accept all cookies
|
|
acceptAllBtn?.addEventListener('click', () => {
|
|
saveConsent({
|
|
analytics: true,
|
|
marketing: true,
|
|
functional: true,
|
|
hasConsented: true,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
});
|
|
|
|
// Reject all cookies
|
|
rejectAllBtn?.addEventListener('click', () => {
|
|
saveConsent({
|
|
analytics: false,
|
|
marketing: false,
|
|
functional: false,
|
|
hasConsented: true,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
});
|
|
|
|
// Show preferences panel
|
|
showPrefsBtn?.addEventListener('click', () => {
|
|
if (mainPanel && prefsPanel) {
|
|
mainPanel.style.display = 'none';
|
|
prefsPanel.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// Save custom preferences
|
|
savePrefsBtn?.addEventListener('click', () => {
|
|
saveConsent({
|
|
analytics: analyticsCheckbox?.checked ?? false,
|
|
marketing: marketingCheckbox?.checked ?? false,
|
|
functional: true, // Always on
|
|
hasConsented: true,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
});
|
|
|
|
// Back to main panel
|
|
backBtn?.addEventListener('click', () => {
|
|
if (mainPanel && prefsPanel) {
|
|
prefsPanel.style.display = 'none';
|
|
mainPanel.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// Check for existing consent on load
|
|
function initBanner() {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
try {
|
|
const parsed = JSON.parse(stored);
|
|
// Already consented - hide banner
|
|
if (banner) {
|
|
banner.setAttribute('hidden', 'true');
|
|
}
|
|
// Sync with nanostore
|
|
consentStore.set(parsed);
|
|
} catch {
|
|
// No valid consent - show banner
|
|
if (banner) {
|
|
banner.removeAttribute('hidden');
|
|
}
|
|
}
|
|
} else {
|
|
// No consent yet - show banner
|
|
if (banner) {
|
|
banner.removeAttribute('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize on page load
|
|
initBanner();
|
|
</script> |