chore: Add PDPA dependencies

- astro-consent: Cookie consent management
- @libsql/client: SQLite database driver
- drizzle-orm: Database ORM for consent logging

Required for full PDPA compliance features.
This commit is contained in:
Kunthawat
2026-03-10 21:25:06 +07:00
parent 2f53e3ed15
commit e98b9f2bff

View File

@@ -0,0 +1,247 @@
---
// Cookie Consent Banner Component - PDPA Compliant
// Displays on first visit, allows users to accept/reject cookie categories
---
<div
id="cookie-consent-banner"
class="fixed bottom-0 left-0 right-0 z-50 bg-white border-t-2 border-primary-600 shadow-2xl p-6 md:p-8 transform translate-y-full transition-transform duration-300 ease-in-out"
role="region"
aria-labelledby="consent-title"
aria-describedby="consent-description"
style="display: none;"
>
<div class="container mx-auto px-4 max-w-7xl">
<div class="flex flex-col lg:flex-row gap-6 items-start lg:items-center justify-between">
<!-- Consent Content -->
<div class="flex-1">
<h2 id="consent-title" class="text-xl md:text-2xl font-bold text-secondary-900 mb-3">
เรายึดถือความเป็นส่วนตัวของคุณ
</h2>
<p id="consent-description" class="text-base md:text-lg text-secondary-700 leading-relaxed mb-4">
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน วิเคราะห์การเข้าใช้งาน และแสดงเนื้อหาที่ตรงใจคุณ
คุณสามารถเลือกยอมรับหรือปฏิเสธคุกกี้ที่ไม่จำเป็นได้
</p>
<!-- Cookie Categories -->
<div class="space-y-3 mt-4">
<!-- Essential Cookies (Always On) -->
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
<input
type="checkbox"
id="consent-essential"
checked
disabled
class="w-5 h-5 accent-primary-600 rounded"
/>
<label for="consent-essential" class="flex-1 cursor-pointer">
<span class="font-semibold text-secondary-900">คุกกี้จำเป็น</span>
<span class="text-sm text-secondary-600 block">ใช้สำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดได้</span>
</label>
<span class="text-xs px-2 py-1 bg-primary-600 text-white rounded">จำเป็น</span>
</div>
<!-- Analytics Cookies -->
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
<input
type="checkbox"
id="consent-analytics"
class="w-5 h-5 accent-primary-600 rounded consent-checkbox"
/>
<label for="consent-analytics" class="flex-1 cursor-pointer">
<span class="font-semibold text-secondary-900">คุกกี้วิเคราะห์ข้อมูล</span>
<span class="text-sm text-secondary-600 block">ช่วยให้เราเข้าใจพฤติกรรมการใช้งาน</span>
</label>
</div>
<!-- Marketing Cookies -->
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
<input
type="checkbox"
id="consent-marketing"
class="w-5 h-5 accent-primary-600 rounded consent-checkbox"
/>
<label for="consent-marketing" class="flex-1 cursor-pointer">
<span class="font-semibold text-secondary-900">คุกกี้การตลาด</span>
<span class="text-sm text-secondary-600 block">ใช้สำหรับแสดงโฆษณาที่เกี่ยวข้อง</span>
</label>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-3 flex-shrink-0">
<button
id="consent-reject"
type="button"
class="bg-secondary-800 hover:bg-secondary-900 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
>
ปฏิเสธทั้งหมด
</button>
<button
id="consent-accept"
type="button"
class="bg-primary-600 hover:bg-primary-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
>
ยอมรับทั้งหมด
</button>
</div>
</div>
<!-- Privacy Policy Link -->
<div class="mt-6 pt-6 border-t border-secondary-200 text-center">
<p class="text-sm text-secondary-600">
การใช้งานคุกกี้ของเราเป็นไปตาม
<a href="/privacy-policy/" class="text-primary-600 hover:underline font-medium">นโยบายความเป็นส่วนตัว</a>
และ
<a href="/terms-and-conditions/" class="text-primary-600 hover:underline font-medium">ข้อกำหนดการใช้งาน</a>
</p>
</div>
</div>
</div>
<script>
interface ConsentPreferences {
essential: boolean;
analytics: boolean;
marketing: boolean;
timestamp: string;
policyVersion: string;
}
const POLICY_VERSION = '1.0.0';
const CONSENT_STORAGE_KEY = 'consent-preferences';
const CONSENT_LOG_API = '/api/consent';
function generateSessionId(): string {
return 'ses_' + Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
function getStoredConsent(): ConsentPreferences | null {
const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
return stored ? JSON.parse(stored) : null;
}
async function saveConsent(consent: ConsentPreferences) {
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(consent));
// Log consent to database (PDPA requirement)
try {
let sessionId = sessionStorage.getItem('consent_session_id');
if (!sessionId) {
sessionId = generateSessionId();
sessionStorage.setItem('consent_session_id', sessionId);
}
await fetch(CONSENT_LOG_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
consent,
policyVersion: POLICY_VERSION,
}),
});
} catch (error) {
console.error('Failed to log consent:', error);
}
}
function showBanner() {
const banner = document.getElementById('cookie-consent-banner') as HTMLElement;
if (banner) {
banner.style.display = 'block';
setTimeout(() => {
banner.classList.remove('translate-y-full');
}, 10);
}
}
function hideBanner() {
const banner = document.getElementById('cookie-consent-banner') as HTMLElement;
if (banner) {
banner.classList.add('translate-y-full');
setTimeout(() => {
banner.style.display = 'none';
}, 300);
}
}
function initConsent() {
const stored = getStoredConsent();
if (stored) {
// Consent already given - restore checkboxes
const analyticsCheckbox = document.getElementById('consent-analytics') as HTMLInputElement;
const marketingCheckbox = document.getElementById('consent-marketing') as HTMLInputElement;
if (analyticsCheckbox) analyticsCheckbox.checked = stored.analytics;
if (marketingCheckbox) marketingCheckbox.checked = stored.marketing;
// Load Umami if consented
if (stored.analytics && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID) {
const script = document.createElement('script');
script.defer = true;
script.src = 'https://analytics.moreminimore.com/script.js';
script.setAttribute('data-website-id', import.meta.env.PUBLIC_UMAMI_WEBSITE_ID);
document.head.appendChild(script);
}
return;
}
// First visit - show banner after delay
setTimeout(showBanner, 500);
}
function handleAccept() {
const analytics = (document.getElementById('consent-analytics') as HTMLInputElement)?.checked ?? false;
const marketing = (document.getElementById('consent-marketing') as HTMLInputElement)?.checked ?? false;
const consent: ConsentPreferences = {
essential: true,
analytics: analytics || true,
marketing: marketing || true,
timestamp: new Date().toISOString(),
policyVersion: POLICY_VERSION,
};
saveConsent(consent);
hideBanner();
}
function handleReject() {
const consent: ConsentPreferences = {
essential: true,
analytics: false,
marketing: false,
timestamp: new Date().toISOString(),
policyVersion: POLICY_VERSION,
};
saveConsent(consent);
hideBanner();
}
// Event listeners
document.addEventListener('DOMContentLoaded', () => {
initConsent();
document.getElementById('consent-accept')?.addEventListener('click', handleAccept);
document.getElementById('consent-reject')?.addEventListener('click', handleReject);
});
// Expose function for footer link
(window as any).openConsentPreferences = () => {
showBanner();
};
</script>
<style>
/* Cookie consent banner styles */
#cookie-consent-banner {
bottom: 0;
left: 0;
right: 0;
}
</style>