Files
opencode-skill/skills/website-creator/templates/consent/ConsentBanner.astro
Kunthawat Greethong 628298183a feat: migrate website-creator from Next.js+Payload to Astro+Tina CMS
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)
2026-04-17 14:52:59 +07:00

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>