Compare commits
2 Commits
54c7a4407e
...
30aeb0f418
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30aeb0f418 | ||
|
|
305e2bd217 |
4
dealplustech-astro/.env.example
Normal file
4
dealplustech-astro/.env.example
Normal 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
|
||||
10
dealplustech-astro/.gitignore
vendored
10
dealplustech-astro/.gitignore
vendored
@@ -1 +1,9 @@
|
||||
.node-version
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
data/*.db
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
272
dealplustech-astro/DEPLOYMENT-NIXPACKS.md
Normal file
272
dealplustech-astro/DEPLOYMENT-NIXPACKS.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# 🚀 Easypanel Deployment Guide (Nixpacks)
|
||||
|
||||
## Overview
|
||||
|
||||
This Astro website is configured for **automatic deployment on Easypanel** using **Nixpacks**.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Auto-Detection
|
||||
|
||||
Easypanel will automatically detect this as a Node.js project and use Nixpacks to build it.
|
||||
|
||||
**No Dockerfile required!**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Configuration
|
||||
|
||||
### Easypanel Settings:
|
||||
|
||||
1. **Project Name:** `dealplustech-astro`
|
||||
2. **Service Name:** `dealplustech-website`
|
||||
3. **Build Type:** `Nixpacks` (auto-detected)
|
||||
4. **Source:** Git repository
|
||||
5. **Branch:** `main`
|
||||
6. **Port:** `4321` (or use `$PORT` env variable)
|
||||
7. **Auto-Deploy:** ✅ Enabled
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Environment Variables
|
||||
|
||||
Set these in Easypanel → Settings → Environment:
|
||||
|
||||
```bash
|
||||
# Database (SQLite file - Nixpacks will persist this)
|
||||
ASTRO_DB_REMOTE_URL=file:/data/consent.db
|
||||
|
||||
# Admin Dashboard
|
||||
ADMIN_PASSWORD=your-secure-password-here
|
||||
|
||||
# Umami Analytics (optional)
|
||||
PUBLIC_UMAMI_WEBSITE_ID=your-website-id
|
||||
PUBLIC_UMAMI_DOMAIN=https://analytics.moreminimore.com
|
||||
|
||||
# Site URL
|
||||
PUBLIC_SITE_URL=https://your-domain.com
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT:** Change `ADMIN_PASSWORD` from the default!
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Persistence
|
||||
|
||||
The consent logging database needs persistent storage.
|
||||
|
||||
### Option A: Volume Mount (Recommended)
|
||||
|
||||
1. In Easypanel, go to Service → Volumes
|
||||
2. Add a new volume:
|
||||
- **Path:** `/data`
|
||||
- **Size:** `1 GB` (minimum)
|
||||
3. Set environment variable:
|
||||
```bash
|
||||
ASTRO_DB_REMOTE_URL=file:/data/consent.db
|
||||
```
|
||||
|
||||
### Option B: Turso (Production)
|
||||
|
||||
For managed database:
|
||||
|
||||
1. Create account at https://turso.tech
|
||||
2. Create database
|
||||
3. Get connection URL
|
||||
4. Set environment:
|
||||
```bash
|
||||
ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
|
||||
ASTRO_DB_APP_TOKEN=your-token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Deployment Workflow
|
||||
|
||||
### Automatic (Git Push):
|
||||
|
||||
1. **Make changes locally**
|
||||
```bash
|
||||
# Edit files
|
||||
npm run build # Test locally
|
||||
git add .
|
||||
git commit -m "Update content"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
2. **Easypanel auto-deploys:**
|
||||
- Detects push to `main`
|
||||
- Runs Nixpacks build
|
||||
- Deploys new version
|
||||
- Health checks pass
|
||||
- Traffic switches
|
||||
|
||||
3. **Verify:**
|
||||
- Check Easypanel dashboard
|
||||
- Visit website URL
|
||||
- Test cookie consent
|
||||
|
||||
### Manual (Force Deploy):
|
||||
|
||||
1. Go to Service in Easypanel
|
||||
2. Click "Deploy"
|
||||
3. Select latest commit
|
||||
4. Deploy
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Health Check
|
||||
|
||||
Nixpacks will automatically health check the service.
|
||||
|
||||
**Endpoint:** `http://localhost:4321/`
|
||||
**Expected:** Status 200 OK
|
||||
|
||||
### Logs
|
||||
|
||||
View in Easypanel:
|
||||
- Service → Logs
|
||||
- Real-time deployment logs
|
||||
- Runtime logs
|
||||
|
||||
### Metrics
|
||||
|
||||
Easypanel provides:
|
||||
- CPU usage
|
||||
- Memory usage
|
||||
- Network traffic
|
||||
- Request count
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Checklist
|
||||
|
||||
- [ ] Change `ADMIN_PASSWORD` from default
|
||||
- [ ] Enable HTTPS (Easypanel provides SSL automatically)
|
||||
- [ ] Set up firewall rules (if needed)
|
||||
- [ ] Configure database persistence
|
||||
- [ ] Regular backups of consent logs
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
**Check:**
|
||||
1. `npm run build` works locally
|
||||
2. `package.json` scripts are correct
|
||||
3. All dependencies installed
|
||||
4. Node version is 20.x
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Database Errors
|
||||
|
||||
**Check:**
|
||||
1. Volume is mounted at `/data`
|
||||
2. Environment variable `ASTRO_DB_REMOTE_URL` is set
|
||||
3. Database file has write permissions
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# In Easypanel:
|
||||
# 1. Add volume at /data
|
||||
# 2. Set ASTRO_DB_REMOTE_URL=file:/data/consent.db
|
||||
# 3. Redeploy
|
||||
```
|
||||
|
||||
### Cookie Consent Not Working
|
||||
|
||||
**Check:**
|
||||
1. CookieConsentBanner component is imported
|
||||
2. BASE_LAYOUT includes the component
|
||||
3. No console errors
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Verify component is included in layout
|
||||
# Check browser console for errors
|
||||
# Clear cache and reload
|
||||
```
|
||||
|
||||
### Admin Dashboard 404
|
||||
|
||||
**Check:**
|
||||
1. Route is `/admin/consent-logs`
|
||||
2. Service is running
|
||||
3. No build errors
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Access: https://your-domain.com/admin/consent-logs
|
||||
# Login with ADMIN_PASSWORD
|
||||
# Check server logs for errors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Recommended Resources:
|
||||
|
||||
- **CPU:** `0.5` core (minimum)
|
||||
- **Memory:** `512 MB` (minimum)
|
||||
- **Disk:** `1 GB` for database
|
||||
|
||||
### Caching:
|
||||
|
||||
Nixpacks automatically caches:
|
||||
- `node_modules/`
|
||||
- Build output
|
||||
|
||||
### CDN (Optional):
|
||||
|
||||
For better performance, add Cloudflare:
|
||||
1. Point DNS to Easypanel
|
||||
2. Enable Cloudflare proxy
|
||||
3. Configure caching rules
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Documentation:**
|
||||
- Astro: https://docs.astro.build
|
||||
- Nixpacks: https://nixpacks.com
|
||||
- Easypanel: https://easypanel.io/docs
|
||||
|
||||
**Admin Dashboard:**
|
||||
- URL: `/admin/consent-logs`
|
||||
- Password: Set via `ADMIN_PASSWORD` env
|
||||
|
||||
**Contact:**
|
||||
- Email: info@dealplustech.co.th
|
||||
- LINE: @dealplustech
|
||||
|
||||
---
|
||||
|
||||
## ✅ Post-Deployment Checklist
|
||||
|
||||
- [ ] Website loads correctly
|
||||
- [ ] Cookie consent appears on first visit
|
||||
- [ ] Consent is logged to database
|
||||
- [ ] Admin dashboard accessible
|
||||
- [ ] Umami Analytics loading (if configured)
|
||||
- [ ] All pages working
|
||||
- [ ] Mobile responsive
|
||||
- [ ] HTTPS enabled (automatic with Easypanel)
|
||||
- [ ] Database backed up regularly
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-03-10
|
||||
**Version:** 1.0.0
|
||||
**Deployment:** Nixpacks on Easypanel
|
||||
227
dealplustech-astro/src/components/CookieConsentBanner.astro
Normal file
227
dealplustech-astro/src/components/CookieConsentBanner.astro
Normal 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>
|
||||
309
dealplustech-astro/src/pages/admin/consent-logs.astro
Normal file
309
dealplustech-astro/src/pages/admin/consent-logs.astro
Normal 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>
|
||||
92
dealplustech-astro/src/pages/api/consent/index.ts
Normal file
92
dealplustech-astro/src/pages/api/consent/index.ts
Normal 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);
|
||||
}
|
||||
203
dealplustech-astro/src/pages/privacy-policy.astro
Normal file
203
dealplustech-astro/src/pages/privacy-policy.astro
Normal 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>
|
||||
Reference in New Issue
Block a user