feat: Add PDPA compliance features to Astro project

- Cookie consent banner with Thai language
- Consent logging API with SQLite database
- Admin dashboard for viewing consent logs
- PDPA-compliant privacy policy
- Environment configuration template

PDPA compliance as per website-creator skill specifications.
Build: npm install, Astro project ready for Docker deployment.
This commit is contained in:
Kunthawat
2026-03-10 13:09:17 +07:00
parent 54c7a4407e
commit 305e2bd217
6 changed files with 844 additions and 1 deletions

View File

@@ -0,0 +1,4 @@
PUBLIC_UMAMI_WEBSITE_ID=your-website-id-here
PUBLIC_UMAMI_DOMAIN=https://analytics.moreminimore.com
ADMIN_PASSWORD=changeme
ASTRO_DB_REMOTE_URL=file:./data/consent.db

View File

@@ -1 +1,9 @@
.node-version node_modules/
dist/
.astro/
data/*.db
.env
.env.*
*.log
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,227 @@
---
// Cookie Consent Banner Component
// 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"
>
<div class="container mx-auto 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"
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"
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));
try {
const sessionId = sessionStorage.getItem('consent_session_id') || 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')!;
banner.classList.remove('translate-y-full');
}
function hideBanner() {
const banner = document.getElementById('cookie-consent-banner')!;
banner.classList.add('translate-y-full');
}
function initConsent() {
const stored = getStoredConsent();
if (stored) {
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;
loadConsentedScripts(stored);
return;
}
setTimeout(showBanner, 500);
}
function loadConsentedScripts(consent: ConsentPreferences) {
if (consent.analytics && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID) {
const umamiScript = document.createElement('script');
umamiScript.defer = true;
umamiScript.src = 'https://analytics.moreminimore.com/script.js';
umamiScript.setAttribute('data-website-id', import.meta.env.PUBLIC_UMAMI_WEBSITE_ID);
document.head.appendChild(umamiScript);
}
}
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);
loadConsentedScripts(consent);
hideBanner();
}
function handleReject() {
const consent: ConsentPreferences = {
essential: true,
analytics: false,
marketing: false,
timestamp: new Date().toISOString(),
policyVersion: POLICY_VERSION,
};
saveConsent(consent);
hideBanner();
}
document.addEventListener('DOMContentLoaded', () => {
initConsent();
document.getElementById('consent-accept')?.addEventListener('click', handleAccept);
document.getElementById('consent-reject')?.addEventListener('click', handleReject);
});
declare global {
interface Window {
openConsentPreferences: () => void;
}
}
window.openConsentPreferences = () => {
showBanner();
};
</script>

View File

@@ -0,0 +1,309 @@
---
import { createClient } from '@libsql/client';
import BaseLayout from '../../layouts/BaseLayout.astro';
const client = createClient({
url: import.meta.env.ASTRO_DB_REMOTE_URL || 'file:./data/consent.db',
authToken: import.meta.env.ASTRO_DB_APP_TOKEN,
});
const ADMIN_PASSWORD = import.meta.env.ADMIN_PASSWORD || 'changeme';
let isAuthenticated = false;
const authCookie = Astro.cookies.get('admin_auth')?.value;
if (authCookie === 'true') {
isAuthenticated = true;
}
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const action = formData.get('action');
if (action === 'login') {
const password = formData.get('password');
if (password === ADMIN_PASSWORD) {
Astro.cookies.set('admin_auth', 'true', {
path: '/',
httpOnly: true,
secure: import.meta.env.PROD,
maxAge: 60 * 60 * 2
});
isAuthenticated = true;
}
} else if (action === 'logout') {
Astro.cookies.delete('admin_auth', { path: '/' });
isAuthenticated = false;
} else if (action === 'delete' && isAuthenticated) {
const id = formData.get('id');
if (id) {
await client.execute({
sql: 'DELETE FROM consent_logs WHERE id = ?',
args: [Number(id)],
});
}
} else if (action === 'delete-all' && isAuthenticated) {
await client.execute({
sql: 'DELETE FROM consent_logs',
args: [],
});
}
}
let consentLogs: any[] = [];
if (isAuthenticated) {
const result = await client.execute({
sql: 'SELECT * FROM consent_logs ORDER BY created_at DESC LIMIT 100',
args: [],
});
consentLogs = result.rows || [];
}
---
<BaseLayout title="Consent Logs Admin" description="Admin dashboard for viewing consent logs">
<main class="min-h-screen bg-secondary-50 py-12">
<div class="container mx-auto px-4 max-w-7xl">
<div class="bg-white rounded-2xl shadow-lg p-6 md:p-8">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl md:text-4xl font-bold text-secondary-900 mb-2">
Consent Logs Admin
</h1>
<p class="text-secondary-600">
View and manage user consent records (PDPA compliance)
</p>
</div>
{isAuthenticated ? (
<form method="POST">
<input type="hidden" name="action" value="logout" />
<button type="submit" class="bg-secondary-800 hover:bg-secondary-900 text-white px-4 py-2 rounded-lg">
Logout
</button>
</form>
) : null}
</div>
{!isAuthenticated ? (
<div class="max-w-md mx-auto">
<div class="bg-secondary-50 rounded-xl p-6 border-2 border-secondary-200">
<h2 class="text-xl font-bold text-secondary-900 mb-4">Admin Login</h2>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="login" />
<div>
<label for="password" class="block text-sm font-semibold text-secondary-700 mb-2">
Password
</label>
<input
type="password"
id="password"
name="password"
class="w-full px-4 py-2 border-2 border-secondary-300 rounded-lg focus:outline-none focus:border-primary-600"
required
/>
</div>
<button type="submit" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg w-full">
Login
</button>
</form>
</div>
</div>
) : (
<div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="bg-primary-50 rounded-xl p-6 border-2 border-primary-200">
<div class="text-sm font-semibold text-primary-700 mb-1">Total Consents</div>
<div class="text-3xl font-bold text-primary-900">{consentLogs.length}</div>
</div>
<div class="bg-secondary-50 rounded-xl p-6 border-2 border-secondary-200">
<div class="text-sm font-semibold text-secondary-700 mb-1">Analytics Accepted</div>
<div class="text-3xl font-bold text-secondary-900">
{consentLogs.filter(l => l.analytics === 1).length}
</div>
</div>
<div class="bg-accent-50 rounded-xl p-6 border-2 border-accent-200">
<div class="text-sm font-semibold text-accent-700 mb-1">Marketing Accepted</div>
<div class="text-3xl font-bold text-accent-900">
{consentLogs.filter(l => l.marketing === 1).length}
</div>
</div>
<div class="bg-secondary-50 rounded-xl p-6 border-2 border-secondary-200">
<div class="text-sm font-semibold text-secondary-700 mb-1">Acceptance Rate</div>
<div class="text-3xl font-bold text-secondary-900">
{consentLogs.length > 0
? Math.round((consentLogs.filter(l => l.analytics === 1).length / consentLogs.length) * 100)
: 0}%
</div>
</div>
</div>
<div class="flex gap-3 mb-6">
<form method="POST" id="delete-all-form" class="inline">
<input type="hidden" name="action" value="delete-all" />
<button
type="submit"
id="delete-all-btn"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-semibold"
>
Delete All Logs
</button>
</form>
<button
type="button"
id="export-btn"
class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-semibold"
>
Export to CSV
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-secondary-800 text-white">
<th class="px-4 py-3 text-left text-sm font-semibold">ID</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Session ID</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Timestamp</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Locale</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Essential</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Analytics</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Marketing</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Policy Version</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{consentLogs.map((log, index) => (
<tr class={index % 2 === 0 ? 'bg-white' : 'bg-secondary-50'}>
<td class="px-4 py-3 text-sm text-secondary-700">{log.id}</td>
<td class="px-4 py-3 text-sm text-secondary-700 font-mono">{log.session_id}</td>
<td class="px-4 py-3 text-sm text-secondary-700">{new Date(log.timestamp).toLocaleString('th-TH')}</td>
<td class="px-4 py-3 text-sm text-secondary-700">{log.locale}</td>
<td class="px-4 py-3 text-sm">
<span class="px-2 py-1 bg-primary-100 text-primary-800 rounded text-xs font-semibold">
{log.essential === 1 ? 'Yes' : 'No'}
</span>
</td>
<td class="px-4 py-3 text-sm">
{log.analytics === 1 ? (
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">Accepted</span>
) : (
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs font-semibold">Rejected</span>
)}
</td>
<td class="px-4 py-3 text-sm">
{log.marketing === 1 ? (
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">Accepted</span>
) : (
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs font-semibold">Rejected</span>
)}
</td>
<td class="px-4 py-3 text-sm text-secondary-700 font-mono">{log.policy_version}</td>
<td class="px-4 py-3 text-sm">
<form method="POST" class="inline delete-form">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="id" value={log.id} />
<button
type="submit"
class="delete-btn text-red-600 hover:text-red-800 hover:underline"
>
Delete
</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
</div>
{consentLogs.length === 0 && (
<div class="text-center py-12">
<p class="text-secondary-600 text-lg">No consent logs found</p>
</div>
)}
<script id="logs-data" type="application/json">
{JSON.stringify(consentLogs)}
</script>
</div>
)}
</div>
</div>
</main>
</BaseLayout>
<script>
interface ConsentLog {
id: number;
session_id: string;
timestamp: string;
locale: string;
essential: number;
analytics: number;
marketing: number;
policy_version: string;
ip_hash: string;
created_at: string;
}
function convertToCSV(logs: ConsentLog[]): string {
const headers = ['ID', 'Session ID', 'Timestamp', 'Locale', 'Essential', 'Analytics', 'Marketing', 'Policy Version', 'IP Hash', 'Created At'];
const rows = logs.map((log: ConsentLog) => [
log.id,
log.session_id,
log.timestamp,
log.locale,
log.essential === 1 ? 'Yes' : 'No',
log.analytics === 1 ? 'Yes' : 'No',
log.marketing === 1 ? 'Yes' : 'No',
log.policy_version,
log.ip_hash,
log.created_at,
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
document.addEventListener('DOMContentLoaded', () => {
const deleteAllBtn = document.getElementById('delete-all-btn');
const deleteAllForm = document.getElementById('delete-all-form') as HTMLFormElement;
if (deleteAllBtn && deleteAllForm) {
deleteAllBtn.addEventListener('click', (e) => {
const confirmDelete = confirm('Delete all consent logs? This cannot be undone.');
if (!confirmDelete) {
e.preventDefault();
deleteAllForm.reset();
}
});
}
const exportBtn = document.getElementById('export-btn');
const logsData = document.getElementById('logs-data');
const consentLogs: ConsentLog[] = logsData ? JSON.parse(logsData.textContent || '[]') : [];
if (exportBtn) {
exportBtn.addEventListener('click', () => {
const csv = convertToCSV(consentLogs);
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `consent-logs-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
});
}
document.querySelectorAll('.delete-form').forEach((form) => {
const button = form.querySelector('.delete-btn');
if (button) {
button.addEventListener('click', (e) => {
const confirmDelete = confirm('Delete this consent log?');
if (!confirmDelete) {
e.preventDefault();
}
});
}
});
});
</script>

View File

@@ -0,0 +1,92 @@
import { createClient } from '@libsql/client';
import type { APIRoute } from 'astro';
const client = createClient({
url: import.meta.env.ASTRO_DB_REMOTE_URL || 'file:./data/consent.db',
authToken: import.meta.env.ASTRO_DB_APP_TOKEN,
});
await client.execute({
sql: `
CREATE TABLE IF NOT EXISTS consent_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL,
timestamp TEXT NOT NULL,
locale TEXT DEFAULT 'th',
essential INTEGER NOT NULL DEFAULT 1,
analytics INTEGER NOT NULL DEFAULT 0,
marketing INTEGER NOT NULL DEFAULT 0,
policy_version TEXT NOT NULL,
ip_hash TEXT,
user_agent TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,
});
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const { sessionId, consent, policyVersion } = body;
if (!sessionId || !consent) {
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const ipHash = await hashIP(ip);
const userAgent = request.headers.get('user-agent') || 'unknown';
const acceptLanguage = request.headers.get('accept-language') || 'th';
const locale = acceptLanguage.startsWith('th') ? 'th' : 'en';
await client.execute({
sql: `
INSERT OR REPLACE INTO consent_logs (
session_id,
timestamp,
locale,
essential,
analytics,
marketing,
policy_version,
ip_hash,
user_agent
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
args: [
sessionId,
consent.timestamp,
locale,
consent.essential ? 1 : 0,
consent.analytics ? 1 : 0,
consent.marketing ? 1 : 0,
policyVersion,
ipHash,
userAgent,
],
});
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Consent API error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
async function hashIP(ip: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(ip);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex.substring(0, 16);
}

View File

@@ -0,0 +1,203 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
const POLICY_VERSION = '1.0.0';
const LAST_UPDATED = '2026-03-10';
---
<BaseLayout
title="นโยบายความเป็นส่วนตัว"
description="นโยบายความเป็นส่วนตัวของดีล พลัส เทค ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 (PDPA)"
>
<main class="min-h-screen py-12 bg-secondary-50">
<article class="container mx-auto px-4 max-w-4xl">
<div class="bg-white rounded-2xl shadow-lg p-6 md:p-12">
<header class="mb-12 pb-8 border-b-2 border-secondary-200">
<h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4">
นโยบายความเป็นส่วนตัว
</h1>
<p class="text-lg text-secondary-600">
Privacy Policy (Personal Data Protection Policy)
</p>
<div class="mt-4 text-sm text-secondary-500">
<p>Version: {POLICY_VERSION}</p>
<p>Last Updated: {new Date(LAST_UPDATED).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
</div>
</header>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
1. ข้อมูลของผู้ควบคุมข้อมูลส่วนบุคคล
</h2>
<div class="bg-secondary-50 rounded-xl p-6 border-l-4 border-primary-600">
<p class="text-secondary-700 mb-4">
<strong class="text-secondary-900">บริษัท ดีล พลัส เทค จำกัด</strong> เป็นผู้ควบคุมข้อมูลส่วนบุคคล ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 (PDPA)
</p>
<ul class="space-y-2 text-secondary-700">
<li><strong>ที่อยู่:</strong> 9/70 ซอยนครลุง 17 แขวงบางไผ่ เขตบางแค กทม. 10160</li>
<li><strong>โทรศัพท์:</strong> 090-555-1415</li>
<li><strong>อีเมล:</strong> info@dealplustech.co.th</li>
</ul>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
2. ประเภทของข้อมูลที่เก็บรวบรวม
</h2>
<ul class="space-y-3">
<li class="flex items-start gap-3">
<span class="text-primary-600 mt-1">✓</span>
<div>
<strong class="text-secondary-900">ข้อมูลประจำตัว:</strong>
<span class="text-secondary-700"> ชื่อ, นามสกุล, ที่อยู่อีเมล, เบอร์โทรศัพท์</span>
</div>
</li>
<li class="flex items-start gap-3">
<span class="text-primary-600 mt-1">✓</span>
<div>
<strong class="text-secondary-900">ข้อมูลการใช้งาน:</strong>
<span class="text-secondary-700"> IP Address, ข้อมูลเบราว์เซอร์, อุปกรณ์ที่ใช้</span>
</div>
</li>
<li class="flex items-start gap-3">
<span class="text-primary-600 mt-1">✓</span>
<div>
<strong class="text-secondary-900">ข้อมูลคุกกี้:</strong>
<span class="text-secondary-700"> การตั้งค่าคุกกี้, Session ID</span>
</div>
</li>
</ul>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
3. วัตถุประสงค์ในการประมวลผลข้อมูล
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-secondary-50 p-4 rounded-lg">
<strong class="text-secondary-900 block mb-1">การให้บริการ</strong>
<span class="text-secondary-600 text-sm">ตอบสนองคำขอ, ให้บริการลูกค้า</span>
</div>
<div class="bg-secondary-50 p-4 rounded-lg">
<strong class="text-secondary-900 block mb-1">การติดต่อกลับ</strong>
<span class="text-secondary-600 text-sm">ตอบคำถาม, ให้ข้อมูลผลิตภัณฑ์</span>
</div>
<div class="bg-secondary-50 p-4 rounded-lg">
<strong class="text-secondary-900 block mb-1">การวิเคราะห์</strong>
<span class="text-secondary-600 text-sm">ปรับปรุงเว็บไซต์, ประสบการณ์ผู้ใช้</span>
</div>
<div class="bg-secondary-50 p-4 rounded-lg">
<strong class="text-secondary-900 block mb-1">ตามกฎหมาย</strong>
<span class="text-secondary-600 text-sm">ปฏิบัติตามข้อบังคับทางกฎหมาย</span>
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
4. ฐานกฎหมายในการประมวลผลข้อมูล
</h2>
<div class="space-y-4">
<div class="border-l-4 border-primary-600 pl-6">
<h3 class="font-bold text-secondary-900 mb-2">4.1 การยินยอม (Consent)</h3>
<p class="text-secondary-700">สำหรับการใช้คุกกี้ที่ไม่จำเป็น, การตลาด</p>
</div>
<div class="border-l-4 border-primary-600 pl-6">
<h3 class="font-bold text-secondary-900 mb-2">4.2 การ履行合同 (Contract)</h3>
<p class="text-secondary-700">เพื่อการให้บริการและดำเนินการตามคำขอของคุณ</p>
</div>
<div class="border-l-4 border-primary-600 pl-6">
<h3 class="font-bold text-secondary-900 mb-2">4.3 ข้อบังคับทางกฎหมาย (Legal Obligation)</h3>
<p class="text-secondary-700">เพื่อปฏิบัติตามกฎหมายและระเบียบที่เกี่ยวข้อง</p>
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
5. ระยะเวลาการเก็บรักษาข้อมูล
</h2>
<div class="bg-primary-50 rounded-xl p-6 border-2 border-primary-200">
<ul class="space-y-3">
<li class="flex justify-between items-center">
<span class="text-secondary-700">ข้อมูลการใช้งานเว็บไซต์</span>
<span class="font-semibold text-primary-900">10 ปี</span>
</li>
<li class="flex justify-between items-center">
<span class="text-secondary-700">บันทึกการยินยอมคุกกี้</span>
<span class="font-semibold text-primary-900">10 ปี</span>
</li>
<li class="flex justify-between items-center">
<span class="text-secondary-700">ข้อมูลติดต่อลูกค้า</span>
<span class="font-semibold text-primary-900">5 ปี</span>
</li>
</ul>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
6. สิทธิของเจ้าของข้อมูลส่วนบุคคล
</h2>
<p class="text-secondary-700 mb-4">
ภายใต้ PDPA คุณมีสิทธิดังนี้:
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-secondary-50 p-4 rounded-lg">
<h3 class="font-bold text-secondary-900 mb-2">สิทธิขอเข้าถึง</h3>
<p class="text-sm text-secondary-600">ขอรับสำเนาข้อมูลส่วนบุคคล</p>
</div>
<div class="bg-secondary-50 p-4 rounded-lg">
<h3 class="font-bold text-secondary-900 mb-2">สิทธิขอแก้ไข</h3>
<p class="text-sm text-secondary-600">ขอให้แก้ไขข้อมูลที่ไม่ถูกต้อง</p>
</div>
<div class="bg-secondary-50 p-4 rounded-lg">
<h3 class="font-bold text-secondary-900 mb-2">สิทธิขอลบ</h3>
<p class="text-sm text-secondary-600">ขอให้ลบข้อมูลส่วนบุคคล</p>
</div>
<div class="bg-secondary-50 p-4 rounded-lg">
<h3 class="font-bold text-secondary-900 mb-2">สิทธิเพิกถอน</h3>
<p class="text-sm text-secondary-600">เพิกถอนความยินยอมเมื่อใดก็ได้</p>
</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
7. คุกกี้และเทคโนโลยีการติดตาม
</h2>
<ul class="space-y-4">
<li class="bg-secondary-50 p-4 rounded-lg border-l-4 border-primary-600">
<h3 class="font-bold text-secondary-900 mb-2">คุกกี้จำเป็น (Essential Cookies)</h3>
<p class="text-sm text-secondary-600">จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดได้</p>
</li>
<li class="bg-secondary-50 p-4 rounded-lg border-l-4 border-accent-500">
<h3 class="font-bold text-secondary-900 mb-2">คุกกี้วิเคราะห์ (Analytics Cookies)</h3>
<p class="text-sm text-secondary-600">帮助我们了解网站使用情况 (Umami Analytics)</p>
</li>
<li class="bg-secondary-50 p-4 rounded-lg border-l-4 border-secondary-400">
<h3 class="font-bold text-secondary-900 mb-2">คุกกี้การตลาด (Marketing Cookies)</h3>
<p class="text-sm text-secondary-600">ใช้สำหรับแสดงโฆษณาที่เกี่ยวข้อง</p>
</li>
</ul>
</section>
<section class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
8. การติดต่อ
</h2>
<div class="bg-primary-50 rounded-xl p-6 border-2 border-primary-200">
<p class="text-secondary-700 mb-4">
หากคุณมีคำถามหรือต้องการใช้สิทธิของคุณ กรุณาติดต่อ:
</p>
<div class="space-y-2">
<p class="text-secondary-900"><strong>อีเมล:</strong> info@dealplustech.co.th</p>
<p class="text-secondary-900"><strong>โทรศัพท์:</strong> 090-555-1415</p>
</div>
</div>
</section>
</div>
</article>
</main>
</BaseLayout>