Refactor: Add full PDPA compliance features

- Cookie consent system (banner + modal) with Thai language
- Consent logging database (Astro DB + SQLite)
- API endpoints for consent management (POST/GET/DELETE)
- Admin dashboard for viewing consent logs (/admin/consent-logs)
- Umami Analytics integration (conditional loading with consent)
- Updated Privacy Policy (full 14-section PDPA Section 36 compliance)
- Updated Terms & Conditions (17 sections, Thailand law)
- Dockerfile updated with SQLite runtime
- Node.js adapter for SSR support
- Admin password: moreminimore2026!Secure (CHANGE IN PRODUCTION)

TODO: Configure Umami Analytics with actual Website ID
This commit is contained in:
Kunthawat Greethong
2026-03-09 13:08:09 +07:00
parent da8437bed0
commit 14ca77ed09
17 changed files with 2372 additions and 43 deletions

View File

@@ -0,0 +1,187 @@
---
// ConsentModal.astro - Standalone consent preferences modal
---
<div
id="consent-modal-standalone"
class="fixed inset-0 z-50 hidden"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity consent-modal-backdrop"></div>
<!-- Modal Panel -->
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div
class="relative transform overflow-hidden rounded-xl bg-white dark:bg-gray-800 shadow-2xl transition-all max-w-lg w-full max-h-[90vh] overflow-y-auto consent-modal-panel"
>
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 id="modal-title" class="text-xl font-bold text-gray-900 dark:text-white">
การตั้งค่าคุกกี้
</h3>
<p class="text-base text-gray-600 dark:text-gray-300 mt-2">
จัดการการตั้งค่าคุกกี้ของคุณที่นี่
</p>
</div>
<!-- Body -->
<div class="px-6 py-4 space-y-4">
<!-- Essential -->
<div class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700">
<input
type="checkbox"
name="consent-essential"
checked
disabled
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้ที่จำเป็น
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดใช้งานได้
</span>
</div>
</div>
<!-- Analytics -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition cursor-pointer">
<input
type="checkbox"
name="consent-analytics"
id="standalone-analytics"
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary consent-checkbox"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้วิเคราะห์ข้อมูล
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
ช่วยเราเข้าใจว่าผู้เยี่ยมชมใช้งานเว็บไซต์ (Umami Analytics)
</span>
</div>
</label>
<!-- Marketing -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition cursor-pointer">
<input
type="checkbox"
name="consent-marketing"
id="standalone-marketing"
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary consent-checkbox"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้การตลาด
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
ใช้สำหรับติดตามและแสดงโฆษณาที่ตรงกับความสนใจของคุณ
</span>
</div>
</label>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex gap-3">
<button
id="consent-modal-save"
class="flex-1 px-6 py-3 bg-secondary hover:bg-secondary-hover text-white rounded-lg font-medium text-base transition focus:outline-none focus:ring-2 focus:ring-secondary focus:ring-offset-2"
>
บันทึกการตั้งค่า
</button>
<button
id="consent-modal-close"
class="flex-1 px-6 py-3 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg font-medium text-base transition hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
>
ปิด
</button>
</div>
</div>
</div>
</div>
</div>
<script>
const CONSENT_KEY = 'consent-preferences';
const modal = document.getElementById('consent-modal-standalone');
const backdrop = modal?.querySelector('.consent-modal-backdrop');
const panel = modal?.querySelector('.consent-modal-panel');
const analyticsCheckbox = document.getElementById('standalone-analytics') as HTMLInputElement;
const marketingCheckbox = document.getElementById('standalone-marketing') as HTMLInputElement;
const btnSave = document.getElementById('consent-modal-save');
const btnClose = document.getElementById('consent-modal-close');
// Open modal function (expose globally)
(window as any).openConsentModal = function() {
modal?.classList.remove('hidden');
// Sync with existing preferences
const existing = localStorage.getItem(CONSENT_KEY);
if (existing) {
try {
const prefs = JSON.parse(existing);
if (analyticsCheckbox) analyticsCheckbox.checked = prefs.analytics || false;
if (marketingCheckbox) marketingCheckbox.checked = prefs.marketing || false;
} catch (e) {
// ignore parse errors
}
}
};
// Close modal function
function closeModal() {
modal?.classList.add('hidden');
}
// Save preferences
async function savePreferences() {
const consentData = {
essential: true,
analytics: analyticsCheckbox?.checked || false,
marketing: marketingCheckbox?.checked || false,
timestamp: new Date().toISOString()
};
localStorage.setItem(CONSENT_KEY, JSON.stringify(consentData));
try {
await fetch('/api/consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: crypto.randomUUID(),
essential: consentData.essential,
analytics: consentData.analytics,
marketing: consentData.marketing,
policyVersion: '1.0',
locale: 'th'
})
});
} catch (error) {
console.error('Failed to save consent:', error);
}
closeModal();
// Dispatch event for other components to listen
window.dispatchEvent(new CustomEvent('consent-updated', { detail: consentData }));
}
// Event listeners
btnSave?.addEventListener('click', savePreferences);
btnClose?.addEventListener('click', closeModal);
backdrop?.addEventListener('click', closeModal);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modal?.classList.contains('hidden')) {
closeModal();
}
});
</script>

View File

@@ -0,0 +1,347 @@
---
// CookieBanner.astro - Simple cookie consent banner with Tailwind CSS
---
<div
id="cookie-banner"
class="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg transform translate-y-full transition-transform duration-300"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Title -->
<h2 class="text-lg font-bold text-gray-900 dark:text-white mb-2">
เราใช้คุกกี้
</h2>
<!-- Description -->
<p class="text-base text-gray-600 dark:text-gray-300 mb-4">
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งานของคุณ และวิเคราะห์การใช้งานเว็บไซต์
</p>
<!-- Checkboxes -->
<div class="space-y-3 mb-6">
<!-- Essential -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700">
<input
type="checkbox"
name="consent-essential"
checked
disabled
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้ที่จำเป็น
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดใช้งานได้
</span>
</div>
</label>
<!-- Analytics -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition cursor-pointer">
<input
type="checkbox"
name="consent-analytics"
id="banner-analytics"
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้วิเคราะห์ข้อมูล
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
ช่วยเราเข้าใจว่าผู้เยี่ยมชมใช้งานเว็บไซต์ (Umami Analytics)
</span>
</div>
</label>
<!-- Marketing -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition cursor-pointer">
<input
type="checkbox"
name="consent-marketing"
id="banner-marketing"
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้การตลาด
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
ใช้สำหรับติดตามและแสดงโฆษณาที่ตรงกับความสนใจของคุณ
</span>
</div>
</label>
</div>
<!-- Buttons -->
<div class="flex flex-col sm:flex-row gap-3">
<button
id="btn-accept-all"
class="flex-1 px-6 py-3 bg-secondary hover:bg-secondary-hover text-white rounded-lg font-medium text-base transition focus:outline-none focus:ring-2 focus:ring-secondary focus:ring-offset-2"
>
ยอมรับทั้งหมด
</button>
<button
id="btn-reject-all"
class="flex-1 px-6 py-3 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg font-medium text-base transition hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
>
ปฏิเสธ
</button>
<button
id="btn-customize"
class="flex-1 px-6 py-3 bg-primary hover:bg-primary-hover text-white rounded-lg font-medium text-base transition focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
ปรับแต่ง
</button>
</div>
</div>
</div>
<!-- Consent Modal -->
<div
id="consent-modal"
class="fixed inset-0 z-50 hidden"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" id="modal-backdrop"></div>
<!-- Modal Panel -->
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div
id="modal-panel"
class="relative transform overflow-hidden rounded-xl bg-white dark:bg-gray-800 shadow-2xl transition-all max-w-lg w-full max-h-[90vh] overflow-y-auto"
>
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 id="modal-title" class="text-xl font-bold text-gray-900 dark:text-white">
การตั้งค่าคุกกี้
</h3>
<p class="text-base text-gray-600 dark:text-gray-300 mt-2">
จัดการการตั้งค่าคุกกี้ของคุณที่นี่
</p>
</div>
<!-- Body -->
<div class="px-6 py-4 space-y-4">
<!-- Essential -->
<div class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700">
<input
type="checkbox"
name="modal-essential"
checked
disabled
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้ที่จำเป็น
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดใช้งานได้
</span>
</div>
</div>
<!-- Analytics -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition cursor-pointer">
<input
type="checkbox"
name="modal-analytics"
id="modal-analytics"
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้วิเคราะห์ข้อมูล
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
ช่วยเราเข้าใจว่าผู้เยี่ยมชมใช้งานเว็บไซต์ (Umami Analytics)
</span>
</div>
</label>
<!-- Marketing -->
<label class="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition cursor-pointer">
<input
type="checkbox"
name="modal-marketing"
id="modal-marketing"
class="mt-1 w-5 h-5 rounded border-gray-300 text-secondary focus:ring-secondary"
/>
<div class="flex-1">
<span class="block font-semibold text-gray-900 dark:text-white text-base">
คุกกี้การตลาด
</span>
<span class="block text-sm text-gray-600 dark:text-gray-400 mt-1">
ใช้สำหรับติดตามและแสดงโฆษณาที่ตรงกับความสนใจของคุณ
</span>
</div>
</label>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex gap-3">
<button
id="btn-save-preferences"
class="flex-1 px-6 py-3 bg-secondary hover:bg-secondary-hover text-white rounded-lg font-medium text-base transition focus:outline-none focus:ring-2 focus:ring-secondary focus:ring-offset-2"
>
บันทึกการตั้งค่า
</button>
<button
id="btn-close-modal"
class="flex-1 px-6 py-3 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg font-medium text-base transition hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
>
ปิด
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Consent Manager
const CONSENT_KEY = 'consent-preferences';
// Get elements
const banner = document.getElementById('cookie-banner');
const modal = document.getElementById('consent-modal');
const modalBackdrop = document.getElementById('modal-backdrop');
const modalPanel = document.getElementById('modal-panel');
// Banner checkboxes
const bannerAnalytics = document.getElementById('banner-analytics') as HTMLInputElement;
const bannerMarketing = document.getElementById('banner-marketing') as HTMLInputElement;
// Modal checkboxes
const modalAnalytics = document.getElementById('modal-analytics') as HTMLInputElement;
const modalMarketing = document.getElementById('modal-marketing') as HTMLInputElement;
// Buttons
const btnAcceptAll = document.getElementById('btn-accept-all');
const btnRejectAll = document.getElementById('btn-reject-all');
const btnCustomize = document.getElementById('btn-customize');
const btnSavePreferences = document.getElementById('btn-save-preferences');
const btnCloseModal = document.getElementById('btn-close-modal');
// Save consent to localStorage and POST to API
async function saveConsent(preferences: { essential: boolean; analytics: boolean; marketing: boolean }) {
const consentData = {
...preferences,
timestamp: new Date().toISOString()
};
// Save to localStorage
localStorage.setItem(CONSENT_KEY, JSON.stringify(consentData));
// POST to API
try {
await fetch('/api/consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: crypto.randomUUID(),
essential: preferences.essential,
analytics: preferences.analytics,
marketing: preferences.marketing,
policyVersion: '1.0',
locale: 'th'
})
});
} catch (error) {
console.error('Failed to save consent:', error);
}
// Hide banner
hideBanner();
}
// Show banner
function showBanner() {
banner?.classList.remove('translate-y-full');
}
// Hide banner
function hideBanner() {
banner?.classList.add('translate-y-full');
}
// Show modal
function showModal() {
modal?.classList.remove('hidden');
// Sync checkbox states from banner
if (modalAnalytics) modalAnalytics.checked = bannerAnalytics?.checked || false;
if (modalMarketing) modalMarketing.checked = bannerMarketing?.checked || false;
}
// Hide modal
function hideModal() {
modal?.classList.add('hidden');
}
// Check if consent already exists
function hasConsent(): boolean {
return localStorage.getItem(CONSENT_KEY) !== null;
}
// Initialize
function init() {
if (!hasConsent()) {
showBanner();
}
}
// Event Listeners
btnAcceptAll?.addEventListener('click', () => {
saveConsent({
essential: true,
analytics: true,
marketing: true
});
});
btnRejectAll?.addEventListener('click', () => {
saveConsent({
essential: true,
analytics: false,
marketing: false
});
});
btnCustomize?.addEventListener('click', () => {
showModal();
});
btnSavePreferences?.addEventListener('click', () => {
saveConsent({
essential: true,
analytics: modalAnalytics?.checked || false,
marketing: modalMarketing?.checked || false
});
hideModal();
});
btnCloseModal?.addEventListener('click', () => {
hideModal();
});
modalBackdrop?.addEventListener('click', () => {
hideModal();
});
// Close modal on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modal?.classList.contains('hidden')) {
hideModal();
}
});
// Run init on DOM ready
init();
</script>

View File

@@ -1,5 +1,7 @@
---
import '../styles/global.css'
import CookieBanner from '../components/consent/CookieBanner.astro';
import ConsentModal from '../components/consent/ConsentModal.astro';
interface Props {
title?: string;
@@ -120,6 +122,7 @@ const { title = 'moreminimore | รับทำเว็บไซต์ฟรี
<div class="flex justify-center gap-6 mb-4">
<a href="/terms-and-conditions" class="hover:text-primary transition">ข้อกำหนดและเงื่อนไข</a>
<a href="/privacy-policy" class="hover:text-primary transition">นโยบายความเป็นส่วนตัว</a>
<button id="consent-preferences-btn" class="hover:text-primary transition">ตั้งค่าคุกกี้</button>
</div>
<p>&copy; {new Date().getFullYear()} moreminimore. สงวนลิขสิทธิ์</p>
</div>
@@ -136,6 +139,36 @@ const { title = 'moreminimore | รับทำเว็บไซต์ฟรี
</a>
</div>
<!-- Cookie Consent Banner -->
<CookieBanner />
<!-- Consent Modal for custom preferences -->
<ConsentModal />
<!-- Consent preferences button in footer should open modal -->
<script is:inline>
const consentBtn = document.getElementById('consent-preferences-btn');
if (consentBtn) {
consentBtn.addEventListener('click', () => {
if (typeof window.openConsentModal === 'function') {
window.openConsentModal();
}
});
}
</script>
<!-- Umami Analytics (Conditional Loading) -->
<script is:inline>
const consent = JSON.parse(localStorage.getItem('consent-preferences') || '{}');
if (consent.analytics) {
const script = document.createElement('script');
script.defer = true;
script.src = 'https://analytics.moreminimore.com/script.js';
script.setAttribute('data-website-id', 'PLACEHOLDER_UMAMI_ID');
document.head.appendChild(script);
}
</script>
<script>
const menuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');

View File

@@ -0,0 +1,188 @@
---
import { getDb } from '../../../db/config';
import schema from '../../../db/schema';
import { desc } from 'drizzle-orm';
const { ConsentLog } = schema.tables;
const db = getDb();
const ADMIN_PASSWORD = import.meta.env.ADMIN_PASSWORD || 'moreminimore';
let consents: typeof ConsentLog.$inferSelect[] = [];
let isAuthenticated = false;
const url = new URL(Astro.request.url);
const action = url.searchParams.get('action');
if (action === 'logout') {
isAuthenticated = false;
consents = [];
throw Astro.redirect('/admin/consent-logs');
}
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const password = formData.get('password');
if (password === ADMIN_PASSWORD) {
isAuthenticated = true;
consents = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
throw Astro.redirect('/admin/consent-logs');
} else {
isAuthenticated = false;
}
} else if (isAuthenticated) {
consents = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
}
---
<!doctype html>
<html lang="th">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin - Consent Logs</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
{isAuthenticated ? (
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-900">บันทึกความยินยอม (Consent Logs)</h1>
<a href="/admin/consent-logs?action=logout" class="text-red-600 hover:underline">ออกจากระบบ</a>
</div>
<div class="bg-white rounded-lg shadow mb-6 p-6">
<div class="grid md:grid-cols-4 gap-4">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="text-sm text-gray-600">ทั้งหมด</div>
<div class="text-2xl font-bold text-blue-600">{consents.length}</div>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<div class="text-sm text-gray-600">ยินยอม Analytics</div>
<div class="text-2xl font-bold text-green-600">{consents.filter(c => c.analytics).length}</div>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<div class="text-sm text-gray-600">ยินยอม Marketing</div>
<div class="text-2xl font-bold text-purple-600">{consents.filter(c => c.marketing).length}</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-600">นโยบายเวอร์ชัน</div>
<div class="text-2xl font-bold text-gray-600">1.0</div>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">วันที่</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Session ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Essential</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Analytics</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Marketing</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP Hash</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ดำเนินการ</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{consents.map((consent) => (
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{new Date(consent.timestamp).toLocaleString('th-TH')}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
{consent.sessionId.substring(0, 8)}...
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
{consent.essential ? '✓' : '✗'}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class={`px-2 py-1 text-xs font-semibold rounded-full ${consent.analytics ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{consent.analytics ? '✓' : '✗'}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class={`px-2 py-1 text-xs font-semibold rounded-full ${consent.marketing ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{consent.marketing ? '✓' : '✗'}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
{consent.ipHash || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<button
onclick={`deleteConsent('${consent.sessionId}')`}
class="text-red-600 hover:text-red-900 hover:underline"
>
ลบ
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="mt-6 text-sm text-gray-600">
<p>แสดง {consents.length} รายการล่าสุด</p>
</div>
</div>
) : (
<div class="min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">เข้าสู่ระบบ Admin</h1>
<form method="post" class="space-y-6">
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">รหัสผ่าน</label>
<input
type="password"
id="password"
name="password"
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-secondary focus:border-transparent"
placeholder="กรอกรหัสผ่าน"
/>
</div>
<button
type="submit"
class="w-full bg-secondary text-white py-3 rounded-lg hover:bg-secondary-hover transition font-medium"
>
เข้าสู่ระบบ
</button>
</form>
<p class="mt-4 text-sm text-gray-600 text-center">
สำหรับจัดการบันทึกความยินยอมคุกกี้
</p>
</div>
</div>
)}
<script>
async function deleteConsent(sessionId: string) {
if (!confirm('ต้องการลบรายการนี้หรือไม่? การกระทำนี้ไม่สามารถย้อนกลับได้')) {
return;
}
try {
const response = await fetch(`/api/consent/${sessionId}`, {
method: 'DELETE'
});
if (response.ok) {
window.location.reload();
} else {
alert('เกิดข้อผิดพลาดในการลบ');
}
} catch (error) {
console.error('Delete error:', error);
alert('เกิดข้อผิดพลาดในการลบ');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,54 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../db/config';
import schema from '../../../../db/schema';
import { eq } from 'drizzle-orm';
export const prerender = false;
const db = getDb();
const { ConsentLog } = schema.tables;
export const GET: APIRoute = async ({ request, url }) => {
try {
const searchParams = new URL(url).searchParams;
const sessionId = searchParams.get('sessionId');
const limit = parseInt(searchParams.get('limit') || '100');
// If sessionId provided, get specific consent
if (sessionId) {
const consent = await db.select()
.from(ConsentLog)
.where(eq(ConsentLog.sessionId, sessionId))
.limit(1);
if (consent.length === 0) {
return new Response(JSON.stringify({ error: 'Consent not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({ success: true, data: consent[0] }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
// Get all consent records (for admin)
const consents = await db.select()
.from(ConsentLog)
.orderBy((t) => t.timestamp)
.limit(limit);
return new Response(JSON.stringify({ success: true, data: consents }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Consent GET error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};

View File

@@ -0,0 +1,81 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../db/config';
import schema from '../../../../db/schema';
import { eq } from 'drizzle-orm';
export const prerender = false;
const db = getDb();
const { ConsentLog } = schema.tables;
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const { sessionId, essential, analytics, marketing, policyVersion, locale } = body;
if (!sessionId) {
return new Response(JSON.stringify({ error: 'Session ID is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || '';
const ipHash = ip ? await hashIP(ip) : '';
const userAgent = request.headers.get('user-agent') || '';
const existing = await db.select().from(ConsentLog).where(eq(ConsentLog.sessionId, sessionId)).limit(1);
if (existing.length > 0) {
await db.update(ConsentLog)
.set({
essential,
analytics,
marketing,
policyVersion,
locale,
ipHash,
userAgent,
timestamp: new Date().toISOString()
})
.where(eq(ConsentLog.sessionId, sessionId));
return new Response(JSON.stringify({ success: true, action: 'updated' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
await db.insert(ConsentLog).values({
sessionId,
timestamp: new Date().toISOString(),
locale: locale || 'th',
essential: essential !== false,
analytics: analytics || false,
marketing: marketing || false,
policyVersion: policyVersion || '1.0',
ipHash,
userAgent
});
return new Response(JSON.stringify({ success: true, action: 'created' }), {
status: 201,
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,51 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../../db/config';
import schema from '../../../../../db/schema';
import { eq } from 'drizzle-orm';
export const prerender = false;
const db = getDb();
const { ConsentLog } = schema.tables;
export const DELETE: APIRoute = async ({ params, request }) => {
try {
// Get sessionId from URL path or query parameter
const url = new URL(request.url);
const sessionId = params.sessionId || url.searchParams.get('sessionId');
if (!sessionId) {
return new Response(JSON.stringify({ error: 'Session ID is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Check if consent exists
const existing = await db.select()
.from(ConsentLog)
.where(eq(ConsentLog.sessionId, sessionId))
.limit(1);
if (existing.length === 0) {
return new Response(JSON.stringify({ error: 'Consent not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
// Delete consent record (Right to be Forgotten - PDPA)
await db.delete(ConsentLog).where(eq(ConsentLog.sessionId, sessionId));
return new Response(JSON.stringify({ success: true, message: 'Consent deleted successfully' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Consent DELETE error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};

View File

@@ -2,7 +2,7 @@
import Layout from '../layouts/Layout.astro'
---
<Layout title="นโยบายความเป็นส่วนตัว | MoreminiMore">
<Layout title="นโยบายความเป็นส่วนตัว | moreminimore">
<section class="py-20 bg-gradient-to-br from-yellow-50 to-white">
<div class="container mx-auto px-4">
<h1 class="text-4xl md:text-5xl font-bold text-center mb-12 text-secondary">
@@ -11,39 +11,145 @@ import Layout from '../layouts/Layout.astro'
<div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-8">
<div class="prose prose-lg max-w-none">
<h2 class="text-2xl font-bold mb-4 text-secondary">1. การเก็บรวบรวมข้อมูล</h2>
<p class="mb-6 text-gray-600">
เราเก็บรวบรวมข้อมูลส่วนบุคคลเฉพาะเมื่อคุณติดต่อเราหรือใช้บริการของเราเท่านั้น
<p class="text-base text-gray-600 mb-8">
นโยบายความเป็นส่วนตัวนี้อธิบายถึงวิธีการที่ moreminimore ("บริษัท", "เรา", หรือ "ของเรา") เก็บรวบรวม ใช้ และเปิดเผยข้อมูลส่วนบุคคลของคุณเมื่อคุณเยี่ยมชมหรือใช้บริการบนเว็บไซต์ moreminimore.com ("เว็บไซต์")
</p>
<p class="text-base text-gray-600 mb-8">
นโยบายความเป็นส่วนตัวนี้สอดคล้องกับพระราชบัญญัติคุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 ("PDPA")
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">2. การใช้ข้อมูล</h2>
<p class="mb-6 text-gray-600">
ข้อมูลของคุณจะถูกใช้เพื่อให้บริการและสื่อสารกับคุณ
<h2 class="text-2xl font-bold mb-4 text-secondary">1. ผู้ควบคุมข้อมูล (Data Controller)</h2>
<p class="mb-4 text-gray-600">
บริษัท moreminimore เป็นผู้ควบคุมข้อมูลส่วนบุคคล ตาม PDPA ซึ่งมีหน้าที่ตัดสินใจเกี่ยวกับการเก็บรวบรวม ใช้ หรือเปิดเผยข้อมูลส่วนบุคคล
</p>
<div class="bg-gray-50 p-4 rounded-lg mb-6">
<p class="text-base mb-2"><strong>ที่อยู่:</strong> กรุงเทพมหานคร, ประเทศไทย</p>
<p class="text-base mb-2"><strong>โทรศัพท์:</strong> 080-995-5945</p>
<p class="text-base mb-2"><strong>อีเมล:</strong> contact@moreminimore.com</p>
</div>
<h2 class="text-2xl font-bold mb-4 text-secondary">2. ประเภทของข้อมูลส่วนบุคคลที่เก็บรวบรวม</h2>
<p class="mb-4 text-gray-600">เราอาจเก็บรวบรวมข้อมูลส่วนบุคคลดังนี้:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li><strong>ข้อมูลระบุตัวตน:</strong> ชื่อ, นามสกุล, ที่อยู่อีเมล, เบอร์โทรศัพท์</li>
<li><strong>ข้อมูลการใช้งาน:</strong> IP address, browser type, device information, pages visited</li>
<li><strong>ข้อมูลคุกกี้:</strong> ข้อมูลจากคุกกี้และเทคโนโลยีการติดตามอื่นๆ (ดูรายละเอียดในนโยบายคุกกี้ด้านล่าง)</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">3. วัตถุประสงค์ในการประมวลผลข้อมูล</h2>
<p class="mb-4 text-gray-600">เราใช้ข้อมูลส่วนบุคคลของคุณเพื่อวัตถุประสงค์ดังนี้:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>ให้บริการและดูแลรักษาเว็บไซต์</li>
<li>ตอบกลับคำถามและคำขอของคุณ</li>
<li>ส่งข้อมูลข่าวสาร การอัปเดต หรือข้อมูลการตลาด (เมื่อคุณยินยอม)</li>
<li>วิเคราะห์การใช้งานเว็บไซต์เพื่อปรับปรุงประสบการณ์ผู้ใช้</li>
<li>ปฏิบัติตามข้อกำหนดทางกฎหมาย</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">4. ฐานกฎหมายในการประมวลผลข้อมูล (Legal Basis)</h2>
<p class="mb-4 text-gray-600">เราประมวลผลข้อมูลส่วนบุคคลของคุณภายใต้ฐานกฎหมายดังนี้:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li><strong>ความยินยอม (Consent):</strong> คุณได้อนุญาตให้เราประมวลผลข้อมูล (เช่น การสมัครรับข่าวสาร)</li>
<li><strong>การปฏิบัติตามสัญญา (Contract):</strong> จำเป็นสำหรับการให้บริการที่คุณขอ</li>
<li><strong>ผลประโยชน์โดยชอบด้วยกฎหมาย (Legitimate Interest):</strong> เพื่อพัฒนาและปรับปรุงบริการของเรา</li>
<li><strong>การปฏิบัติตามกฎหมาย (Legal Obligation):</strong> เมื่อจำเป็นต้องปฏิบัติตามข้อกำหนดทางกฎหมาย</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">5. ระยะเวลาการเก็บรักษาข้อมูล</h2>
<p class="mb-4 text-gray-600">
เราเก็บรักษาข้อมูลส่วนบุคคลของคุณตราบเท่าที่จำเป็นเพื่อวัตถุประสงค์ที่ระบุไว้ในนโยบายนี้ หรือตามที่กฎหมายกำหนด โดยทั่วไป:
</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>ข้อมูลการติดต่อ: เก็บรักษาจนกว่าคุณจะขอถอนความยินยอมหรือลบข้อมูล</li>
<li>ข้อมูลการใช้งาน: เก็บรักษาไม่เกิน 2 ปี</li>
<li>ข้อมูลคุกกี้: เก็บรักษาตามประเภทของคุกกี้ (ดูรายละเอียดในนโยบายคุกกี้)</li>
<li><strong>บันทึกความยินยอม:</strong> เก็บรักษาอย่างน้อย 10 ปี เพื่อปฏิบัติตาม PDPA</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">6. การเปิดเผยข้อมูลให้แก่บุคคลที่สาม</h2>
<p class="mb-4 text-gray-600">เราไม่ขายหรือให้เช่าข้อมูลส่วนบุคคลของคุณ เราอาจเปิดเผยข้อมูลแก่:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li><strong>ผู้ให้บริการ:</strong> Hosting providers, analytics providers (เช่น Umami Analytics)</li>
<li><strong>หน่วยงานรัฐบาล:</strong> เมื่อจำเป็นตามกฎหมายหรือคำสั่งศาล</li>
<li><strong>ที่ปรึกษา:</strong> นักกฎหมาย, นักบัญชี, หรือที่ปรึกษาอื่นๆ ภายใต้ข้อตกลงการรักษาความลับ</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">7. การโอนข้อมูลข้ามประเทศ</h2>
<p class="mb-4 text-gray-600">
ข้อมูลส่วนบุคคลของคุณอาจถูกโอนไปยังประเทศนอกประเทศไทย ในกรณีที่ประเทศปลายทางไม่มีมาตรฐานการคุ้มครองข้อมูลที่เพียงพอ เราจะใช้มาตรการคุ้มครองที่เหมาะสม เช่น Standard Contractual Clauses
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">3. การปกป้องข้อมูล</h2>
<p class="mb-6 text-gray-600">
ราใช้มาตรการรักษาความปลอดภัยที่เหมาะสมเพื่อปกป้องข้อมูลส่วนบุคคลของคุณ
<h2 class="text-2xl font-bold mb-4 text-secondary">8. การตัดสินใจโดยอัตโนมัติ</h2>
<p class="mb-4 text-gray-600">
ว็บไซต์ของเราไม่ใช้การตัดสินใจโดยอัตโนมัติหรือการกำหนดโปรไฟล์ที่ส่งผลกระทบอย่างมีนัยสำคัญต่อคุณ
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">4. การเปิดเผยข้อมูล</h2>
<p class="mb-6 text-gray-600">
เราจะไม่ขายหรือให้เช่าข้อมูลส่วนบุคคลของคุณให้ฝ่ายที่สาม
<h2 class="text-2xl font-bold mb-4 text-secondary">9. คุกกี้และเทคโนโลยีการติดตาม</h2>
<p class="mb-4 text-gray-600">เราใช้คุกกี้และเทคโนโลยียอดนิยมเพื่อ:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li><strong>คุกกี้ที่จำเป็น (Essential):</strong> จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดใช้งานได้</li>
<li><strong>คุกกี้วิเคราะห์ข้อมูล (Analytics):</strong> ช่วยเราเข้าใจการใช้งานเว็บไซต์ (Umami Analytics) - ต้องได้รับความยินยอม</li>
<li><strong>คุกกี้การตลาด (Marketing):</strong> ใช้สำหรับโฆษณา - ต้องได้รับความยินยอม</li>
</ul>
<p class="mb-4 text-gray-600">
คุณสามารถจัดการการตั้งค่าคุกกี้ได้ตลอดเวลาโดยคลิกปุ่ม "ตั้งค่าคุกกี้" ในส่วนท้ายของเว็บไซต์ หรือถอนความยินยอมผ่านแบนเนอร์คุกกี้
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">5. คุกกี้</h2>
<p class="mb-6 text-gray-600">
เว็บไซต์ของเราอาจใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน
<h2 class="text-2xl font-bold mb-4 text-secondary">10. สิทธิ์ของเจ้าของข้อมูล (Data Subject Rights)</h2>
<p class="mb-4 text-gray-600">ภายใต้ PDPA คุณมีสิทธิ์ดังนี้:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li><strong>สิทธิ์ในการเข้าถึง (Right to Access):</strong> ขอสำเนาข้อมูลส่วนบุคคลของคุณ</li>
<li><strong>สิทธิ์ในการแก้ไข (Right to Rectification):</strong> ขอแก้ไขข้อมูลที่ไม่ถูกต้อง</li>
<li><strong>สิทธิ์ในการลบ (Right to Erasure):</strong> ขอลบข้อมูลส่วนบุคคลของคุณ</li>
<li><strong>สิทธิ์ในการระงับการใช้ข้อมูล (Right to Restriction):</strong> ขอระงับการใช้ข้อมูล</li>
<li><strong>สิทธิ์ในการโอนย้ายข้อมูล (Right to Data Portability):</strong> ขอโอนข้อมูลไปยังผู้ควบคุมข้อมูลรายอื่น</li>
<li><strong>สิทธิ์ในการคัดค้าน (Right to Object):</strong> คัดค้านการประมวลผลข้อมูล</li>
<li><strong>สิทธิ์ในการถอนความยินยอม (Right to Withdraw Consent):</strong> ถอนความยินยอมเมื่อใดก็ได้</li>
</ul>
<p class="mb-4 text-gray-600">
หากต้องการใช้สิทธิ์เหล่านี้ กรุณาติดต่อเราที่ contact@moreminimore.com
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">6. สิทธิ์ของคุณ</h2>
<p class="mb-6 text-gray-600">
คุณมีสิทธิ์ในการขอเข้าถึง แก้ไข หรือลบข้อมูลส่วนบุคคลของคุณ
<h2 class="text-2xl font-bold mb-4 text-secondary">11. มาตรการรักษาความปลอดภัย</h2>
<p class="mb-4 text-gray-600">เราใช้มาตรการรักษาความปลอดภัยที่เหมาะสม:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>การเข้ารหัสข้อมูล (HTTPS/SSL)</li>
<li>การควบคุมการเข้าถึงข้อมูล</li>
<li>การสำรองข้อมูลเป็นประจำ</li>
<li>การฝึกอบรมพนักงานเรื่องการคุ้มครองข้อมูล</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">12. เจ้าหน้าที่คุ้มครองข้อมูล (DPO)</h2>
<p class="mb-4 text-gray-600">
หากคุณมีคำถามเกี่ยวกับการคุ้มครองข้อมูล คุณสามารถติดต่อเราได้ที่ contact@moreminimore.com
</p>
<p class="mt-8 text-gray-500 text-base">
อัปเดตล่าสุด: {new Date().toLocaleDateString('th-TH')}
<h2 class="text-2xl font-bold mb-4 text-secondary">13. สิทธิ์ในการร้องเรียน</h2>
<p class="mb-4 text-gray-600">
หากคุณเชื่อว่าเราละเมิด PDPA คุณมีสิทธิ์ร้องเรียนต่อคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล (PDPC) ผ่านสำนักงานคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล
</p>
<div class="bg-gray-50 p-4 rounded-lg mb-6">
<p class="text-base mb-2"><strong>สำนักงานคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล</strong></p>
<p class="text-base mb-2">โทรศัพท์: 1212</p>
<p class="text-base">เว็บไซต์: www.pdpc.or.th</p>
</div>
<h2 class="text-2xl font-bold mb-4 text-secondary">14. การเปลี่ยนแปลงนโยบายความเป็นส่วนตัว</h2>
<p class="mb-4 text-gray-600">
เราอาจอัปเดตนโยบายความเป็นส่วนตัวนี้เป็นครั้งคราว เราจะแจ้งให้คุณทราบเกี่ยวกับการเปลี่ยนแปลงที่สำคัญโดยการโพสต์นโยบายใหม่บนเว็บไซต์นี้
</p>
<div class="mt-12 pt-8 border-t border-gray-200">
<p class="text-base text-gray-500">
<strong>เวอร์ชัน:</strong> 1.0
</p>
<p class="text-base text-gray-500">
<strong>มีผลบังคับใช้:</strong> {new Date().toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
<p class="text-base text-gray-500">
<strong>อัปเดตล่าสุด:</strong> {new Date().toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
</div>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@
import Layout from '../layouts/Layout.astro'
---
<Layout title="ข้อกำหนดและเงื่อนไข | MoreminiMore">
<Layout title="ข้อกำหนดและเงื่อนไข | moreminimore">
<section class="py-20 bg-gradient-to-br from-yellow-50 to-white">
<div class="container mx-auto px-4">
<h1 class="text-4xl md:text-5xl font-bold text-center mb-12 text-secondary">
@@ -11,37 +11,139 @@ import Layout from '../layouts/Layout.astro'
<div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-8">
<div class="prose prose-lg max-w-none">
<p class="text-base text-gray-600 mb-8">
กรุณาอ่านข้อกำหนดและเงื่อนไขเหล่านี้อย่างละเอียดก่อนใช้บริการเว็บไซต์ moreminimore.com ("เว็บไซต์") และบริการของบริษัท moreminimore ("บริษัท", "เรา", หรือ "ของเรา")
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">1. การยอมรับเงื่อนไข</h2>
<p class="mb-6 text-gray-600">
การใช้เว็บไซต์และบริการของ MoreminiMore Co.,Ltd. แสดงว่าคุณยอมรับและตกลงที่จะปฏิบัติตามข้อกำหนดและเงื่อนไขเหล่านี้
<p class="mb-4 text-gray-600">
การเข้าถึงหรือใช้บริการเว็บไซต์และบริการของเรา แสดงว่าคุณยอมรับและตกลงที่จะปฏิบัติตามข้อกำหนดและเงื่อนไขเหล่านี้ ตลอดจนนโยบายความเป็นส่วนตัวของเรา หากคุณไม่ยอมรับเงื่อนไขเหล่านี้ กรุณาอย่าใช้บริการของเรา
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">2. บริการ</h2>
<p class="mb-6 text-gray-600">
เราให้บริการที่ปรึกษาองค์กรดิจิตอล ที่ปรึกษาการตลาดออนไลน์ พัฒนาเว็บไซต์ พัฒนาแอปพลิเคชัน และระบบแชทบอท
รายละเอียดบริการเป็นไปตามที่ตกลงกันในสัญญา
<h2 class="text-2xl font-bold mb-4 text-secondary">2. บริการของเรา</h2>
<p class="mb-4 text-gray-600">เราให้บริการดังนี้:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>Web Development - พัฒนาเว็บไซต์ด้วย WordPress และ Astro</li>
<li>AI Automation Setup - ติดตั้งระบบ AI เพื่อลดงานซ้ำซ้อน</li>
<li>AI Consult & Implementation - ที่ปรึกษาและติดตั้งระบบ AI</li>
<li>IT Services - บริการด้านไอทีสำหรับ SMEs</li>
</ul>
<p class="mb-4 text-gray-600">
รายละเอียดบริการจะเป็นไปตามที่ตกลงกันในสัญญาหรือใบเสนอราคาแยกต่างหาก
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">3. ทรัพย์สินทางปัญญา</h2>
<p class="mb-6 text-gray-600">
เนื้อหาทั้งหมดบนเว็บไซต์นี้ รวมถึงข้อความ รูปภาพ โลโก้ และซอฟต์แวร์ เป็นทรัพย์สินของ MoreminiMore Co.,Ltd.
ห้ามคัดลอกหรือใช้โดยไม่ได้รับอนุญาต
<h2 class="text-2xl font-bold mb-4 text-secondary">3. การใช้เว็บไซต์</h2>
<p class="mb-4 text-gray-600">คุณตกลงที่จะ:</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>ใช้เว็บไซต์เพื่อวัตถุประสงค์ที่ถูกกฎหมายเท่านั้น</li>
<li>ไม่พยายามเข้าถึงระบบหรือข้อมูลโดยไม่ได้รับอนุญาต</li>
<li>ไม่ใช้เว็บไซต์ในทางที่ผิดหรือก่อให้เกิดความเสียหาย</li>
<li>ไม่คัดลอกหรือใช้เนื้อหาจากเว็บไซต์โดยไม่ได้รับอนุญาต</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">4. ทรัพย์สินทางปัญญา</h2>
<p class="mb-4 text-gray-600">
เนื้อหาทั้งหมดบนเว็บไซต์นี้ รวมถึงแต่ไม่จำกัดเพียง ข้อความ รูปภาพ กราฟิก โลโก้ ไอคอน ภาพ เสียง การดาวน์โหลดซอฟต์แวร์ และทรัพย์สินอื่นๆ เป็นทรัพย์สินทางปัญญาของ moreminimore หรือผู้ให้ใบอนุญาตของเรา และได้รับความคุ้มครองตามกฎหมายทรัพย์สินทางปัญญาของประเทศไทย
</p>
<p class="mb-4 text-gray-600">
ห้ามมิให้ทำซ้ำ คัดลอก ดัดแปลง เผยแพร่ หรือใช้ประโยชน์จากเนื้อหาใด ๆ บนเว็บไซต์นี้โดยไม่ได้รับอนุญาตเป็นลายลักษณ์อักษรจากเรา
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">4. ความรับผิดชอบ</h2>
<p class="mb-6 text-gray-600">
เราให้คำปรึกษาและบริการตามความสามารถ แต่ไม่สามารถรับประกันผลลัพธ์ทางธุรกิจที่เฉพาะเจาะจงได้
ผลลัพธ์ขึ้นอยู่กับหลายปัจจัยนอกเหนือจากการควบคุมของเรา
<h2 class="text-2xl font-bold mb-4 text-secondary">5. ข้อมูลส่วนบุคคล</h2>
<p class="mb-4 text-gray-600">
การใช้ข้อมูลส่วนบุคคลของคุณเป็นไปตามนโยบายความเป็นส่วนตัวของเรา ซึ่งสอดคล้องกับ PDPA กรุณาอ่านนโยบายความเป็นส่วนตัวเพื่อเข้าใจวิธีการเก็บรวบรวม ใช้ และปกป้องข้อมูลของคุณ
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">5. การแก้ไขเงื่อนไข</h2>
<p class="mb-6 text-gray-600">
ราขอสงวนสิทธิ์ในการแก้ไขข้อกำหนดและเงื่อนไขนี้ได้ทุกเวลา โดยไม่ต้องแจ้งให้ทราบล่วงหน้
<h2 class="text-2xl font-bold mb-4 text-secondary">6. คุกกี้</h2>
<p class="mb-4 text-gray-600">
ว็บไซต์ของเราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน เมื่อคุณเยี่ยมชมเว็บไซต์ครั้งแรก คุณจะเห็นแบนเนอร์คุกกี้เพื่อขอความยินยอม คุณสามารถจัดการการตั้งค่าคุกกี้ได้ตลอดเวล
</p>
<p class="mt-8 text-gray-500 text-base">
อัปเดตล่าสุด: {new Date().toLocaleDateString('th-TH')}
<h2 class="text-2xl font-bold mb-4 text-secondary">7. ข้อจำกัดความรับผิดชอบ</h2>
<p class="mb-4 text-gray-600">
ข้อมูลบนเว็บไซต์นี้มีไว้เพื่อวัตถุประสงค์ในการให้ข้อมูลทั่วไปเท่านั้น แม้ว่าเราจะพยายามให้ข้อมูลที่ถูกต้องและทันสมัย แต่เราไม่รับรองว่า:
</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>ข้อมูลบนเว็บไซต์จะถูกต้องครบถ้วน สมบูรณ์ หรือเป็นปัจจุบัน</li>
<li>เว็บไซต์จะพร้อมใช้งานตลอดเวลา หรือปราศจากข้อผิดพลาด</li>
<li>ข้อผิดพลาดใดๆ บนเว็บไซต์จะถูกแก้ไข</li>
</ul>
<p class="mb-4 text-gray-600">
สำหรับบริการที่ปรึกษา เราให้คำแนะนำตามความสามารถและประสบการณ์ แต่ไม่สามารถรับประกันผลลัพธ์ทางธุรกิจที่เฉพาะเจาะจงได้ ผลลัพธ์ขึ้นอยู่กับหลายปัจจัยนอกเหนือจากการควบคุมของเรา
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">8. การจำกัดความรับผิด</h2>
<p class="mb-4 text-gray-600">
ภายใต้กฎหมายที่บังคับใช้เท่านั้น moreminimore จะไม่รับผิดชอบต่อบุคคลหรือนิติบุคคลใดๆ สำหรับ:
</p>
<ul class="list-disc list-inside mb-6 text-gray-600 space-y-2">
<li>ความเสียหายโดยตรง โดยตรงเป็นพิเศษ หรือโดยอ้อมใดๆ</li>
<li>การสูญเสียรายได้ กำไร หรือข้อมูลทางธุรกิจ</li>
<li>ความเสียหายที่เกิดขึ้นจากการใช้หรือไม่สามารถใช้บริการของเรา</li>
</ul>
<h2 class="text-2xl font-bold mb-4 text-secondary">9. ลิงก์ไปยังเว็บไซต์ภายนอก</h2>
<p class="mb-4 text-gray-600">
เว็บไซต์ของเราอาจมีลิงก์ไปยังเว็บไซต์ของบุคคลที่สาม เราไม่ควบคุมและไม่ต้องรับผิดชอบสำหรับเนื้อหา นโยบายความเป็นส่วนตัว หรือการปฏิบัติของเว็บไซต์เหล่านั้น
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">10. การชดเชย</h2>
<p class="mb-4 text-gray-600">
คุณตกลงที่จะชดเชยและปกป้อง moreminimore จากข้อเรียกร้อง ค่าเสียหาย หรือค่าใช้จ่ายใดๆ ที่เกิดขึ้นจากการใช้บริการของเรา หรือการละเมิดข้อกำหนดและเงื่อนไขเหล่านี้
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">11. การสิ้นสุดบริการ</h2>
<p class="mb-4 text-gray-600">
เราขอสงวนสิทธิ์ในการระงับหรือยกเลิกการเข้าถึงเว็บไซต์ของคุณ หาก我们发现คุณละเมิดข้อกำหนดและเงื่อนไขเหล่านี้ โดยไม่ต้องแจ้งให้ทราบล่วงหน้า
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">12. กฎหมายที่ใช้บังคับ</h2>
<p class="mb-4 text-gray-600">
ข้อกำหนดและเงื่อนไขเหล่านี้จะอยู่ภายใต้และตีความตามกฎหมายของราชอาณาจักรไทย ศาลไทยมีอำนาจแต่เพียงผู้เดียวในการพิจารณาคดีใดๆ ที่เกิดขึ้นจากหรือเกี่ยวข้องกับข้อกำหนดเหล่านี้
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">13. การระงับข้อพิพาท</h2>
<p class="mb-4 text-gray-600">
ในกรณีที่เกิดข้อพิพาทจากข้อกำหนดและเงื่อนไขนี้ คู่สัญญาทั้งสองฝ่ายตกลงที่จะเจรจาไกล่เกลี่ยข้อพิพาทก่อน หากไม่สามารถตกลงกันได้ภายใน 30 วัน จึงจะนำคดีขึ้นสู่ศาล
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">14. การแก้ไขข้อกำหนด</h2>
<p class="mb-4 text-gray-600">
เราขอสงวนสิทธิ์ในการแก้ไขข้อกำหนดและเงื่อนไขนี้ได้ทุกเวลา โดยการแก้ไขจะมีผลทันทีเมื่อโพสต์บนเว็บไซต์นี้ คุณควรตรวจสอบหน้านี้เป็นระยะเพื่อดูการเปลี่ยนแปลง
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">15. การแยกส่วน</h2>
<p class="mb-4 text-gray-600">
หากข้อกำหนดใดข้อกำหนดหนึ่งถูกพิจารณาว่าไม่ถูกต้องหรือบังคับใช้ไม่ได้ ส่วนที่เหลือของข้อกำหนดยังคงมีผลบังคับใช้เต็มที่
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">16. การสละสิทธิ์</h2>
<p class="mb-4 text-gray-600">
การที่เราไม่บังคับใช้สิทธิ์หรือบทบัญญัติใด ๆ ของข้อกำหนดและเงื่อนไขนี้ ไม่ถือเป็นการสละสิทธิ์นั้น หรือบทบัญญัติใดๆ
</p>
<h2 class="text-2xl font-bold mb-4 text-secondary">17. ข้อมูลติดต่อ</h2>
<p class="mb-4 text-gray-600">
หากคุณมีคำถามเกี่ยวกับข้อกำหนดและเงื่อนไขเหล่านี้ กรุณาติดต่อเรา:
</p>
<div class="bg-gray-50 p-4 rounded-lg mb-6">
<p class="text-base mb-2"><strong>moreminimore</strong></p>
<p class="text-base mb-2">กรุงเทพมหานคร, ประเทศไทย</p>
<p class="text-base mb-2"><strong>โทรศัพท์:</strong> 080-995-5945</p>
<p class="text-base mb-2"><strong>อีเมล:</strong> contact@moreminimore.com</p>
</div>
<div class="mt-12 pt-8 border-t border-gray-200">
<p class="text-base text-gray-500">
<strong>เวอร์ชัน:</strong> 1.0
</p>
<p class="text-base text-gray-500">
<strong>มีผลบังคับใช้:</strong> {new Date().toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
<p class="text-base text-gray-500">
<strong>อัปเดตล่าสุด:</strong> {new Date().toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
</div>
</div>
</div>
</div>