Update skills: add website-creator, mql-developer, ecommerce-astro

Changes:
- Add FAL_KEY and GEMINI_API_KEY to .env.example
- Update picture-it to use ~/.config/opencode/.env (unified creds)
- Remove shodh-memory skill (no longer used)
- Remove alphaear-* skills (deprecated)
- Remove thai-frontend-dev skill (replaced by website-creator)
- Remove theme-factory skill
- Add mql-developer skill (MQL5 trading)
- Add ecommerce-astro skill (Astro e-commerce)
- Add website-creator skill (Next.js + Payload CMS)
- Update install script for new skills
This commit is contained in:
2026-04-16 17:40:27 +07:00
parent 5053ccdba2
commit b26c8199a5
562 changed files with 59030 additions and 37600 deletions

View File

@@ -0,0 +1,69 @@
import type { CollectionConfig } from 'payload'
import { admins, adminsOnly, adminsOrSelf, anyone } from './access'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: {
forgotPassword: {
generateEmailHTML: ({ token }) => {
const resetPasswordURL = `${process.env.SERVER_URL}/reset-password?token=${token}`
return `
<!doctype html>
<html>
<body>
<p>คุณได้รับอีเมลนี้เนื่องจากมีการขอตั้ง密码ใหม่สำหรับบัญชีของคุณ</p>
<p>กรุณาคลิกที่ลิงก์ด้านล่างเพื่อตั้ง密码ใหม่:</p>
<a href="${resetPasswordURL}">${resetPasswordURL}</a>
<p>หากคุณไม่ได้เป็นผู้ร้องขอ กรุณาเพิกเฉยต่ออีเมลนี้</p>
</body>
</html>
`
},
},
},
access: {
create: anyone, // Allow anyone to create a user account (for registration)
read: adminsOrSelf, // Allow users to read their own profile, admins can read all
update: adminsOrSelf, // Allow users to update their own profile, admins can update all
delete: admins, // Only admins can delete users
admin: adminsOnly,
},
fields: [
{
name: 'role',
type: 'select',
options: [
{ label: 'ผู้ดูแลระบบ', value: 'admin' },
{ label: 'ผู้ใช้งาน', value: 'user' },
],
defaultValue: 'user',
required: true,
access: {
read: adminsOnly,
create: adminsOnly,
update: adminsOnly,
},
},
{
name: 'firstName',
type: 'text',
required: true,
admin: {
description: 'ชื่อจริง',
},
},
{
name: 'lastName',
type: 'text',
required: true,
admin: {
description: 'นามสกุล',
},
},
// Email is added by Payload auth automatically
// Password is handled by Payload auth automatically
],
}

View File

@@ -0,0 +1,44 @@
import type { Access } from 'payload'
import type { User } from '../../payload-types'
// Utility function to check if user has specific roles
export const checkRole = (allRoles: User['role'][] = [], user: User | null = null): boolean => {
if (user) {
if (allRoles.some((role) => user?.role === role)) {
return true
}
}
return false
}
// Common access patterns
export const anyone: Access = () => true
export const admins: Access = ({ req: { user } }) => checkRole(['admin'], user)
export const adminsOnly: Access = ({ req: { user } }: { req: { user: User | null } }) =>
checkRole(['admin'], user)
export const authenticated: Access = ({ req: { user } }) => !!user
export const adminsOrSelf: Access = ({ req: { user } }) => {
if (!user) return false
if (checkRole(['admin'], user)) return true
return {
id: {
equals: user.id,
},
}
}
export const adminsOrOwner = (ownerField: string = 'user'): Access => {
return ({ req: { user } }) => {
if (!user) return false
if (checkRole(['admin'], user)) return true
return {
[ownerField]: {
equals: user.id,
},
}
}
}

View File

@@ -0,0 +1,462 @@
---
// CookieConsent.astro - PDPA Cookie Consent Banner
// ทำงานจริง: ถ้า reject จะไม่ load tracking scripts
interface Props {
position?: 'bottom' | 'top';
theme?: 'light' | 'dark';
}
const { position = 'bottom', theme = 'light' } = Astro.props;
// Consent states
const CONSENT_TYPES = {
ESSENTIAL: 'essential',
ANALYTICS: 'analytics',
MARKETING: 'marketing',
FUNCTIONAL: 'functional',
} as const;
---
<div id="cookie-consent-banner" class={`cookie-consent cookie-consent--${position} cookie-consent--${theme}`} hidden>
<div class="cookie-consent__content">
<div class="cookie-consent__text">
<h3 class="cookie-consent__title">นโยบายคุกกี้</h3>
<p class="cookie-consent__description">
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งานเว็บไซต์ของคุณ
คุณสามารถเลือกได้ว่าจะอนุญาตคุกกี้ประเภทใด
<a href="/privacy-policy" target="_blank">อ่านนโยบายความเป็นส่วนตัว</a>
</p>
</div>
<div class="cookie-consent__categories">
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้ที่จำเป็น</span>
<span class="cookie-consent__badge cookie-consent__badge--required">จำเป็นเสมอ</span>
</div>
<p class="cookie-consent__category-desc">ใช้สำหรับการทำงานพื้นฐานของเว็บไซต์ ไม่สามารถปิดได้</p>
</div>
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้วิเคราะห์</span>
<label class="cookie-consent__toggle">
<input type="checkbox" id="consent-analytics" checked />
<span class="cookie-consent__toggle-slider"></span>
</label>
</div>
<p class="cookie-consent__category-desc">ช่วยให้เราเข้าใจพฤติกรรมการใช้งานเว็บไซต์</p>
</div>
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้การตลาด</span>
<label class="cookie-consent__toggle">
<input type="checkbox" id="consent-marketing" checked />
<span class="cookie-consent__toggle-slider"></span>
</label>
</div>
<p class="cookie-consent__category-desc">ใช้สำหรับแสดงโฆษณาที่ตรงกับความสนใจของคุณ</p>
</div>
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้ฟังก์ชัน</span>
<label class="cookie-consent__toggle">
<input type="checkbox" id="consent-functional" checked />
<span class="cookie-consent__toggle-slider"></span>
</label>
</div>
<p class="cookie-consent__category-desc">ช่วยจดจำการตั้งค่าของคุณ</p>
</div>
</div>
<div class="cookie-consent__actions">
<button id="cookie-consent-accept-all" class="cookie-consent__btn cookie-consent__btn--primary">
ยอมรับทั้งหมด
</button>
<button id="cookie-consent-reject-all" class="cookie-consent__btn cookie-consent__btn--secondary">
ปฏิเสธทั้งหมด
</button>
<button id="cookie-consent-save" class="cookie-consent__btn cookie-consent__btn--outline">
บันทึกการตั้งค่า
</button>
</div>
</div>
</div>
<style>
.cookie-consent {
position: fixed;
left: 0;
right: 0;
z-index: 9999;
background: var(--color-bg, #ffffff);
border-top: 1px solid var(--color-border, #e5e7eb);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
font-family: 'Kanit', 'Noto Sans Thai', system-ui, sans-serif;
}
.cookie-consent--bottom {
bottom: 0;
}
.cookie-consent--top {
top: 0;
}
.cookie-consent[hidden] {
display: none;
}
.cookie-consent__content {
max-width: 800px;
margin: 0 auto;
}
.cookie-consent__title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: var(--color-text, #111827);
}
.cookie-consent__description {
font-size: 0.875rem;
color: var(--color-text-secondary, #6b7280);
margin: 0 0 1rem 0;
line-height: 1.6;
}
.cookie-consent__description a {
color: var(--color-primary, #3b82f6);
text-decoration: underline;
}
.cookie-consent__categories {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.cookie-consent__category {
background: var(--color-bg-secondary, #f9fafb);
border-radius: 0.5rem;
padding: 1rem;
}
.cookie-consent__category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.cookie-consent__category-name {
font-weight: 500;
color: var(--color-text, #111827);
}
.cookie-consent__badge {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-weight: 500;
}
.cookie-consent__badge--required {
background: var(--color-primary, #3b82f6);
color: white;
}
.cookie-consent__category-desc {
font-size: 0.8125rem;
color: var(--color-text-secondary, #6b7280);
margin: 0;
line-height: 1.5;
}
.cookie-consent__toggle {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.cookie-consent__toggle input {
opacity: 0;
width: 0;
height: 0;
}
.cookie-consent__toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--color-border, #d1d5db);
border-radius: 24px;
transition: 0.3s;
}
.cookie-consent__toggle-slider::before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.cookie-consent__toggle input:checked + .cookie-consent__toggle-slider {
background: var(--color-primary, #3b82f6);
}
.cookie-consent__toggle input:checked + .cookie-consent__toggle-slider::before {
transform: translateX(20px);
}
.cookie-consent__toggle input:disabled + .cookie-consent__toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
.cookie-consent__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.cookie-consent__btn {
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
border: none;
}
.cookie-consent__btn--primary {
background: var(--color-primary, #3b82f6);
color: white;
}
.cookie-consent__btn--primary:hover {
background: var(--color-primary-dark, #2563eb);
}
.cookie-consent__btn--secondary {
background: var(--color-text, #111827);
color: white;
}
.cookie-consent__btn--secondary:hover {
background: var(--color-text-dark, #000000);
}
.cookie-consent__btn--outline {
background: transparent;
color: var(--color-text, #111827);
border: 1px solid var(--color-border, #d1d5db);
}
.cookie-consent__btn--outline:hover {
background: var(--color-bg-secondary, #f9fafb);
}
/* Dark theme */
.cookie-consent--dark {
--color-bg: #1f2937;
--color-bg-secondary: #374151;
--color-border: #4b5563;
--color-text: #f9fafb;
--color-text-secondary: #d1d5db;
--color-primary: #60a5fa;
--color-primary-dark: #3b82f6;
}
@media (max-width: 640px) {
.cookie-consent {
padding: 1rem;
}
.cookie-consent__actions {
flex-direction: column;
}
.cookie-consent__btn {
width: 100%;
}
}
</style>
<script>
// Consent Manager
class ConsentManager {
private readonly CONSENT_KEY = 'cookie_consent';
private readonly API_URL = '/api/consent';
async init() {
// Check if consent already given
const existing = this.getStoredConsent();
if (!existing) {
this.showBanner();
} else {
this.applyConsent(existing);
}
// Bind event listeners
this.bindEvents();
}
private bindEvents() {
const acceptAll = document.getElementById('cookie-consent-accept-all');
const rejectAll = document.getElementById('cookie-consent-reject-all');
const save = document.getElementById('cookie-consent-save');
acceptAll?.addEventListener('click', () => this.acceptAll());
rejectAll?.addEventListener('click', () => this.rejectAll());
save?.addEventListener('click', () => this.saveCustom());
}
private showBanner() {
const banner = document.getElementById('cookie-consent-banner');
banner?.removeAttribute('hidden');
}
private hideBanner() {
const banner = document.getElementById('cookie-consent-banner');
banner?.setAttribute('hidden', '');
}
private getStoredConsent(): Record<string, boolean> | null {
const stored = localStorage.getItem(this.CONSENT_KEY);
return stored ? JSON.parse(stored) : null;
}
private async saveConsent(consent: Record<string, boolean>) {
// Save to localStorage
localStorage.setItem(this.CONSENT_KEY, JSON.stringify(consent));
// Send to server
try {
await fetch(this.API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...consent,
session_id: this.getSessionId(),
}),
});
} catch (error) {
console.error('Failed to save consent:', error);
}
// Apply consent
this.applyConsent(consent);
this.hideBanner();
}
private async acceptAll() {
const consent = {
essential: true,
analytics: true,
marketing: true,
functional: true,
};
await this.saveConsent(consent);
}
private async rejectAll() {
const consent = {
essential: true, // Always required
analytics: false,
marketing: false,
functional: false,
};
await this.saveConsent(consent);
}
private async saveCustom() {
const consent = {
essential: true, // Always required
analytics: (document.getElementById('consent-analytics') as HTMLInputElement)?.checked ?? false,
marketing: (document.getElementById('consent-marketing') as HTMLInputElement)?.checked ?? false,
functional: (document.getElementById('consent-functional') as HTMLInputElement)?.checked ?? false,
};
await this.saveConsent(consent);
}
private applyConsent(consent: Record<string, boolean>) {
// Essential cookies - always on (handled by server)
// Analytics
if (consent.analytics) {
this.enableAnalytics();
} else {
this.disableAnalytics();
}
// Marketing
if (consent.marketing) {
this.enableMarketing();
} else {
this.disableMarketing();
}
// Functional
if (consent.functional) {
this.enableFunctional();
} else {
this.disableFunctional();
}
}
private enableAnalytics() {
// Enable GA4 etc.
window.dispatchEvent(new CustomEvent('consent:analytics:Granted'));
}
private disableAnalytics() {
// Disable GA4, clear existing cookies
window.dispatchEvent(new CustomEvent('consent:analytics:Denied'));
}
private enableMarketing() {
window.dispatchEvent(new CustomEvent('consent:marketing:Granted'));
}
private disableMarketing() {
window.dispatchEvent(new CustomEvent('consent:marketing:Denied'));
}
private enableFunctional() {
window.dispatchEvent(new CustomEvent('consent:functional:Granted'));
}
private disableFunctional() {
window.dispatchEvent(new CustomEvent('consent:functional:Denied'));
}
private getSessionId(): string {
let sessionId = sessionStorage.getItem('session_id');
if (!sessionId) {
sessionId = crypto.randomUUID();
sessionStorage.setItem('session_id', sessionId);
}
return sessionId;
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new ConsentManager().init();
});
</script>

View File

@@ -0,0 +1,99 @@
# PDPA Consent Logging Template
Template สำหรับเพิ่ม PDPA consent logging ใน Next.js + Payload CMS (MongoDB)
## Files
```
consent/
├── collections/
│ └── ConsentLogs.ts # Payload collection สำหรับ consent logs
├── api/
│ └── route.ts # API endpoint สำหรับบันทึก consent
├── cookie-banner.tsx # CookieBanner component
└── README.md
```
## วิธีใช้
### 1. เพิ่ม ConsentLogs Collection
Copy `collections/ConsentLogs.ts` ไปที่ `src/collections/` ของ project
### 2. สร้าง API Endpoint
Copy `api/route.ts` ไปที่ `src/app/api/consent/route.ts`
### 3. เพิ่ม CookieBanner Component
Copy `cookie-banner.tsx` ไปที่ `src/components/`
### 4. เพิ่มใน Layout
เพิ่ม `<CookieBanner />` ใน `src/app/(frontend)/layout.tsx`:
```tsx
import { CookieBanner } from '@/components/cookie-banner'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<CookieBanner />
</body>
</html>
)
}
```
### 5. เพิ่ม Collection ใน payload.config.ts
```ts
import ConsentLogs from './collections/ConsentLogs'
export default buildConfig({
collections: [Users, Media, Snacks, Orders, ConsentLogs],
// ...
})
```
## API
### POST /api/consent
บันทึก consent action
**Request:**
```json
{
"action": "accept",
"purpose": "all",
"analytics": true,
"marketing": false,
"functional": true
}
```
**Response:**
```json
{
"success": true,
"doc": {
"id": "...",
"action": "accept",
"purpose": "all",
"analytics": true,
"marketing": false,
"functional": true,
"userAgent": "Mozilla/5.0...",
"ip": "127.0.0.1",
"timestamp": "2026-04-10T00:00:00.000Z"
}
}
```
## ⚠️ Pitfalls สำคัญ
1. **ใช้ `mongooseAdapter` ไม่ใช่ `mongodbAdapter`**
2. **ConsentLogs ต้องใช้ `export default`** ไม่ใช่ named export

View File

@@ -0,0 +1,81 @@
import type { APIRoute } from 'astro'
// POST /api/consent - บันทึก consent
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json()
const { session_id, essential, analytics, marketing, functional } = body
if (!session_id) {
return new Response(JSON.stringify({ error: 'session_id is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// Get client info
const ipAddress = request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown'
const userAgent = request.headers.get('user-agent') || 'unknown'
// Build consent record
const consentTypes = []
if (essential) consentTypes.push('essential')
if (analytics) consentTypes.push('analytics')
if (marketing) consentTypes.push('marketing')
if (functional) consentTypes.push('functional')
// In Payload CMS, you would save this to the consent-logs collection
// For now, return success (Payload integration happens at build time)
const record = {
sessionId: session_id,
consentType: consentTypes.length === 4 ? 'accept_all' : consentTypes.join(','),
granted: analytics || marketing || functional,
ipAddress,
userAgent,
metadata: { essential, analytics, marketing, functional },
createdAt: new Date().toISOString(),
}
// Log for debugging (remove in production)
console.log('[Consent API] New consent record:', JSON.stringify(record))
return new Response(JSON.stringify({ success: true, record }), {
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' },
})
}
}
// GET /api/consent - ตรวจสอบ consent ของ session
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url)
const sessionId = url.searchParams.get('session_id')
if (!sessionId) {
return new Response(JSON.stringify({ error: 'session_id is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// In Payload CMS, query the consent-logs collection
// For now, return not found (Payload integration happens at build time)
return new Response(JSON.stringify({ error: 'Not implemented in template' }), {
status: 501,
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' },
})
}
}

View File

@@ -0,0 +1,86 @@
import type { APIRoute } from 'astro'
// Right to be Forgotten API - PDPA Article 17
// DELETE /api/consent?session_id=xxx - ลบข้อมูลของ session นี้
export const DELETE: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url)
const sessionId = url.searchParams.get('session_id')
if (!sessionId) {
return new Response(
JSON.stringify({ error: 'session_id is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
// In Payload CMS, you would:
// 1. Find all consent-logs with this sessionId
// 2. Delete them
// 3. Also delete any user data associated with this session
// Example Payload query (for reference):
// await payload.delete({
// collection: 'consent-logs',
// where: { sessionId: { equals: sessionId } },
// })
console.log(`[Right to be Forgotten] Deleting data for session: ${sessionId}`)
return new Response(
JSON.stringify({
success: true,
message: 'ข้อมูลของคุณถูกลบแล้ว',
deletedAt: new Date().toISOString(),
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('[Right to be Forgotten] Error:', error)
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
}
// GET /api/consent/export - ขอ export ข้อมูลของตัวเอง (PDPA Article 31)
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url)
const sessionId = url.searchParams.get('session_id')
if (!sessionId) {
return new Response(
JSON.stringify({ error: 'session_id is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
// In Payload CMS, query consent-logs for this session
// Return the data as JSON for the user to review
// Example Payload query (for reference):
// const logs = await payload.find({
// collection: 'consent-logs',
// where: { sessionId: { equals: sessionId } },
// })
return new Response(
JSON.stringify({
success: true,
message: 'ข้อมูลของคุณ',
data: [], // Replace with actual Payload query result
requestedAt: new Date().toISOString(),
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('[Consent Export] Error:', error)
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
}

View File

@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@/payload.config'
/**
* POST /api/consent - Record consent action
*
* Request body:
* {
* action: 'accept' | 'reject' | 'update',
* purpose: 'analytics' | 'marketing' | 'functional' | 'all',
* analytics: boolean,
* marketing: boolean,
* functional: boolean,
* previousConsent?: { analytics: boolean, marketing: boolean, functional: boolean }
* }
*/
export async function POST(request: NextRequest) {
try {
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const body = await request.json()
const { action, purpose, analytics, marketing, functional, previousConsent } = body
// Validate required fields
if (!action || !['accept', 'reject', 'update'].includes(action)) {
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
}
if (!purpose || !['analytics', 'marketing', 'functional', 'all'].includes(purpose)) {
return NextResponse.json({ error: 'Invalid purpose' }, { status: 400 })
}
// Get IP and User Agent
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]
|| request.headers.get('x-real-ip')
|| 'unknown'
const userAgent = request.headers.get('user-agent') || 'unknown'
// Create consent log
const consentLog = await payload.create({
collection: 'consent-logs',
data: {
action,
purpose,
analytics: analytics ?? false,
marketing: marketing ?? false,
functional: functional ?? false,
userAgent,
ip,
timestamp: new Date().toISOString(),
previousConsent: previousConsent || null,
newConsent: {
analytics: analytics ?? false,
marketing: marketing ?? false,
functional: functional ?? false,
},
},
})
return NextResponse.json({ success: true, doc: consentLog })
} catch (error) {
console.error('Consent logging error:', error)
return NextResponse.json({ error: 'Failed to log consent' }, { status: 500 })
}
}
/**
* GET /api/consent - Get current consent status (from cookie or localStorage)
* This endpoint is mainly for verification, actual consent is stored client-side
*/
export async function GET(request: NextRequest) {
// Consent is stored client-side in localStorage
// This endpoint is for compliance verification
return NextResponse.json({
message: 'Consent is stored client-side',
purposes: ['analytics', 'marketing', 'functional'],
note: 'Use POST to update consent preferences'
})
}

View File

@@ -0,0 +1,188 @@
import { CollectionConfig, Field } from 'payload'
// Consent Log Collection - เก็บ log การยินยอมของ users
export const ConsentLog: CollectionConfig = {
slug: 'consent-logs',
admin: {
useAsTitle: 'sessionId',
defaultColumns: ['sessionId', 'consentType', 'granted', 'createdAt'],
description: 'บันทึกการยินยอมของผู้ใช้ตาม PDPA',
},
access: {
// ทุกคนสามารถสร้าง log ได้ (public)
create: () => true,
// แต่ดูได้เฉพาะ admin
read: ({ req: { user } }) => {
if (!user) return false
return user.role === 'admin'
},
// แก้ไขได้เฉพาะ admin
update: ({ req: { user } }) => {
if (!user) return false
return user.role === 'admin'
},
// ลบได้เฉพาะ admin
delete: ({ req: { user } }) => {
if (!user) return false
return user.role === 'admin'
},
},
fields: [
{
name: 'sessionId',
type: 'text',
required: true,
admin: {
description: 'Session ID ของผู้ใช้',
},
},
{
name: 'consentType',
type: 'select',
required: true,
options: [
{ label: 'Essential', value: 'essential' },
{ label: 'Analytics', value: 'analytics' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Functional', value: 'functional' },
{ label: 'All Accepted', value: 'accept_all' },
{ label: 'All Rejected', value: 'reject_all' },
],
},
{
name: 'granted',
type: 'checkbox',
required: true,
defaultValue: false,
admin: {
description: 'ยินยอมหรือไม่',
},
},
{
name: 'ipAddress',
type: 'text',
admin: {
description: 'IP Address ของผู้ใช้',
readOnly: true,
},
},
{
name: 'userAgent',
type: 'text',
admin: {
description: 'Browser User Agent',
readOnly: true,
},
},
{
name: 'metadata',
type: 'json',
admin: {
description: 'ข้อมูลเพิ่มเติม',
},
},
{
name: 'createdAt',
type: 'date',
required: true,
admin: {
description: 'วันที่และเวลาที่ยินยอม',
readOnly: true,
},
},
],
hooks: {
beforeChange: [
({ data }) => {
// เพิ่ม timestamp อัตโนมัติ
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
return data
},
],
},
}
// Consent Settings Collection - เก็บ settings ของ consent banner
export const ConsentSettings: CollectionConfig = {
slug: 'consent-settings',
admin: {
useAsTitle: 'title',
description: 'ตั้งค่า Cookie Consent Banner',
},
access: {
read: () => true, // Public read
create: ({ req: { user } }) => !!user && user.role === 'admin',
update: ({ req: { user } }) => !!user && user.role === 'admin',
delete: ({ req: { user } }) => !!user && user.role === 'admin',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'นโยบายคุกกี้',
},
{
name: 'description',
type: 'textarea',
required: true,
defaultValue: 'เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งานเว็บไซต์ของคุณ คุณสามารถเลือกได้ว่าจะอนุญาตคุกกี้ประเภทใด',
},
{
name: 'position',
type: 'select',
defaultValue: 'bottom',
options: [
{ label: 'ด้านล่าง (Bottom)', value: 'bottom' },
{ label: 'ด้านบน (Top)', value: 'top' },
],
},
{
name: 'theme',
type: 'select',
defaultValue: 'light',
options: [
{ label: 'Light Mode', value: 'light' },
{ label: 'Dark Mode', value: 'dark' },
],
},
{
name: 'essentialCookies',
type: 'json',
admin: {
description: 'รายชื่อ essential cookies ที่จำเป็นต้องมี',
},
},
{
name: 'analyticsCookies',
type: 'json',
admin: {
description: 'รายชื่อ analytics cookies',
},
},
{
name: 'marketingCookies',
type: 'json',
admin: {
description: 'รายชื่อ marketing cookies',
},
},
{
name: 'functionalCookies',
type: 'json',
admin: {
description: 'รายชื่อ functional cookies',
},
},
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
admin: {
description: 'แสดง consent banner หรือไม่',
},
},
],
}

View File

@@ -0,0 +1,122 @@
import type { CollectionConfig } from 'payload'
export interface ConsentLogData {
action: 'accept' | 'reject' | 'update'
purpose: 'analytics' | 'marketing' | 'functional' | 'all'
userAgent?: string
ip?: string
timestamp: string
previousConsent?: Record<string, boolean>
newConsent?: Record<string, boolean>
}
const ConsentLogs: CollectionConfig = {
slug: 'consent-logs',
admin: {
useAsTitle: 'timestamp',
defaultColumns: ['timestamp', 'action', 'purpose', 'ip'],
description: 'Log of all consent actions for PDPA compliance',
},
access: {
create: () => true, // Allow anyone to create consent logs (public endpoint)
read: () => true, // Allow reading for compliance purposes
update: () => false, // Consent logs should not be modified
delete: () => false, // Consent logs should not be deleted
},
fields: [
{
name: 'action',
type: 'select',
required: true,
options: [
{ label: 'Accept', value: 'accept' },
{ label: 'Reject', value: 'reject' },
{ label: 'Update', value: 'update' },
],
admin: {
description: 'The type of consent action',
},
},
{
name: 'purpose',
type: 'select',
required: true,
options: [
{ label: 'Analytics', value: 'analytics' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Functional', value: 'functional' },
{ label: 'All', value: 'all' },
],
admin: {
description: 'The purpose of the consent',
},
},
{
name: 'analytics',
type: 'checkbox',
defaultValue: false,
admin: {
description: 'Consent for analytics cookies',
},
},
{
name: 'marketing',
type: 'checkbox',
defaultValue: false,
admin: {
description: 'Consent for marketing cookies',
},
},
{
name: 'functional',
type: 'checkbox',
defaultValue: false,
admin: {
description: 'Consent for functional cookies',
},
},
{
name: 'userAgent',
type: 'text',
admin: {
readOnly: true,
description: 'Browser user agent string',
},
},
{
name: 'ip',
type: 'text',
admin: {
readOnly: true,
description: 'IP address of the user',
},
},
{
name: 'timestamp',
type: 'date',
required: true,
admin: {
readOnly: true,
description: 'When the consent was given',
},
},
{
name: 'previousConsent',
type: 'json',
admin: {
readOnly: true,
description: 'Previous consent state (for updates)',
},
},
{
name: 'newConsent',
type: 'json',
admin: {
readOnly: true,
description: 'New consent state',
},
},
],
}
export default ConsentLogs

View File

@@ -0,0 +1,316 @@
'use client'
import { useState, useEffect } from 'react'
interface ConsentState {
analytics: boolean
marketing: boolean
functional: boolean
hasConsented: boolean
timestamp?: string
}
const defaultConsent: ConsentState = {
analytics: false,
marketing: false,
functional: false,
hasConsented: false,
}
const STORAGE_KEY = 'pdpa_consent'
export function CookieBanner() {
const [consent, setConsent] = useState<ConsentState>(defaultConsent)
const [showBanner, setShowBanner] = useState(false)
const [showPreferences, setShowPreferences] = useState(false)
// Load consent from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
const parsed = JSON.parse(stored)
setConsent(parsed)
setShowBanner(false)
} catch {
setShowBanner(true)
}
} else {
setShowBanner(true)
}
}, [])
// Save consent to localStorage
const saveConsent = async (newConsent: ConsentState) => {
// Save to localStorage
localStorage.setItem(STORAGE_KEY, JSON.stringify(newConsent))
setConsent(newConsent)
setShowBanner(false)
setShowPreferences(false)
// Log to server
try {
await fetch('/api/consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: newConsent.hasConsented ? 'accept' : 'reject',
purpose: 'all',
...newConsent,
}),
})
} catch (error) {
console.error('Failed to log consent:', error)
}
}
// Accept all cookies
const acceptAll = () => {
saveConsent({
analytics: true,
marketing: true,
functional: true,
hasConsented: true,
timestamp: new Date().toISOString(),
})
}
// Reject all cookies (only functional)
const rejectAll = () => {
saveConsent({
analytics: false,
marketing: false,
functional: false,
hasConsented: true,
timestamp: new Date().toISOString(),
})
}
// Save custom preferences
const savePreferences = () => {
saveConsent({
...consent,
hasConsented: true,
timestamp: new Date().toISOString(),
})
}
// Update individual preference
const updatePreference = (key: keyof Pick<ConsentState, 'analytics' | 'marketing' | 'functional'>, value: boolean) => {
setConsent(prev => ({ ...prev, [key]: value }))
}
// If no banner to show, return null
if (!showBanner) return null
return (
<div
style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#ffffff',
boxShadow: '0 -4px 20px rgba(0, 0, 0, 0.15)',
padding: '1.5rem',
zIndex: 9999,
borderTop: '1px solid #e5e5e5',
}}
role="dialog"
aria-label="Cookie Consent Banner"
>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
{!showPreferences ? (
// Main banner
<div>
<h3 style={{ margin: '0 0 0.75rem 0', fontSize: '1.125rem', fontWeight: 600 }}>
🍪 PDPA Cookie Consent
</h3>
<p style={{ margin: '0 0 1rem 0', color: '#555', fontSize: '0.9375rem', lineHeight: 1.5 }}>
We use cookies to enhance your experience. By continuing to visit this site, you agree to our use of cookies.{' '}
<a href="/privacy-policy" style={{ color: '#0066cc' }}>
Learn more
</a>
</p>
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
<button
onClick={acceptAll}
style={{
padding: '0.625rem 1.25rem',
backgroundColor: '#22c55e',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '0.9375rem',
fontWeight: 500,
cursor: 'pointer',
}}
>
Accept All Cookies
</button>
<button
onClick={rejectAll}
style={{
padding: '0.625rem 1.25rem',
backgroundColor: '#f5f5f5',
color: '#333',
border: '1px solid #ddd',
borderRadius: '6px',
fontSize: '0.9375rem',
fontWeight: 500,
cursor: 'pointer',
}}
>
Reject All
</button>
<button
onClick={() => setShowPreferences(true)}
style={{
padding: '0.625rem 1.25rem',
backgroundColor: 'transparent',
color: '#0066cc',
border: '1px solid #0066cc',
borderRadius: '6px',
fontSize: '0.9375rem',
fontWeight: 500,
cursor: 'pointer',
}}
>
Cookie Preferences
</button>
</div>
</div>
) : (
// Preferences panel
<div>
<h3 style={{ margin: '0 0 0.75rem 0', fontSize: '1.125rem', fontWeight: 600 }}>
Cookie Preferences
</h3>
<p style={{ margin: '0 0 1rem 0', color: '#555', fontSize: '0.875rem' }}>
Manage your cookie preferences below.
</p>
<div style={{ marginBottom: '1rem' }}>
{/* Functional Cookies */}
<div style={{
padding: '1rem',
backgroundColor: '#f9f9f9',
borderRadius: '8px',
marginBottom: '0.75rem',
border: '1px solid #e5e5e5'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
<div>
<h4 style={{ margin: 0, fontSize: '0.9375rem', fontWeight: 600 }}>Functional Cookies</h4>
<p style={{ margin: '0.25rem 0 0 0', fontSize: '0.8125rem', color: '#666' }}>
Essential for the website to function properly. Cannot be disabled.
</p>
</div>
<div style={{
padding: '0.25rem 0.75rem',
backgroundColor: '#e5e5e5',
color: '#666',
borderRadius: '4px',
fontSize: '0.75rem',
fontWeight: 500,
}}>
Always Active
</div>
</div>
</div>
{/* Analytics Cookies */}
<div style={{
padding: '1rem',
backgroundColor: '#fff',
borderRadius: '8px',
marginBottom: '0.75rem',
border: '1px solid #e5e5e5'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h4 style={{ margin: 0, fontSize: '0.9375rem', fontWeight: 600 }}>Analytics Cookies</h4>
<p style={{ margin: '0.25rem 0 0 0', fontSize: '0.8125rem', color: '#666' }}>
Help us understand how visitors interact with our website.
</p>
</div>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="checkbox"
checked={consent.analytics}
onChange={(e) => updatePreference('analytics', e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
/>
</label>
</div>
</div>
{/* Marketing Cookies */}
<div style={{
padding: '1rem',
backgroundColor: '#fff',
borderRadius: '8px',
marginBottom: '0.75rem',
border: '1px solid #e5e5e5'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h4 style={{ margin: 0, fontSize: '0.9375rem', fontWeight: 600 }}>Marketing Cookies</h4>
<p style={{ margin: '0.25rem 0 0 0', fontSize: '0.8125rem', color: '#666' }}>
Used to track visitors across websites for advertising purposes.
</p>
</div>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="checkbox"
checked={consent.marketing}
onChange={(e) => updatePreference('marketing', e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
/>
</label>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '0.75rem' }}>
<button
onClick={savePreferences}
style={{
padding: '0.625rem 1.25rem',
backgroundColor: '#0066cc',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '0.9375rem',
fontWeight: 500,
cursor: 'pointer',
}}
>
Save Preferences
</button>
<button
onClick={() => setShowPreferences(false)}
style={{
padding: '0.625rem 1.25rem',
backgroundColor: 'transparent',
color: '#666',
border: 'none',
borderRadius: '6px',
fontSize: '0.9375rem',
cursor: 'pointer',
}}
>
Back
</button>
</div>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
node_modules
.next
out
dist
build
*.log
.env*.local
.DS_Store
*.pem

View File

@@ -0,0 +1,5 @@
# Payload CMS
PAYLOAD_SECRET=your-secret-key-here-change-in-production
# Database (PostgreSQL) - database name must match POSTGRES_DB in docker-compose
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/payload

View File

@@ -0,0 +1,39 @@
# Multi-stage Dockerfile for Next.js + Payload CMS with PostgreSQL
# Requires `output: 'standalone'` in next.config.ts
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate && pnpm install --frozen-lockfile
FROM deps AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -0,0 +1,40 @@
version: '3'
services:
payload:
image: node:22-alpine
ports:
- '3000:3000'
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
working_dir: /home/node/app/
command: sh -c "corepack enable && corepack prepare pnpm@9.0.0 --activate && pnpm install && pnpm dev"
depends_on:
- postgres
env_file:
- .env
networks:
- payload-network
postgres:
restart: always
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- '5432:5432'
environment:
POSTGRES_USER: payload
POSTGRES_PASSWORD: payloadpass
POSTGRES_DB: payload
networks:
- payload-network
networks:
payload-network:
driver: bridge
volumes:
pgdata:
node_modules:

View File

@@ -0,0 +1,31 @@
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(__filename)
const nextConfig: NextConfig = {
images: {
localPatterns: [
{
pathname: '/api/media/file/**',
},
],
},
output: 'standalone',
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
return webpackConfig
},
turbopack: {
root: path.resolve(dirname),
},
}
export default withPayload(nextConfig, { devBundleServerPackages: false })

View File

@@ -0,0 +1,45 @@
{
"name": "nextjs-payload-starter",
"version": "1.0.0",
"description": "Next.js + Payload CMS starter template with PostgreSQL",
"private": true,
"type": "module",
"scripts": {
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"docker:dev": "docker compose up -d",
"docker:dev:logs": "docker compose logs -f",
"docker:down": "docker compose down"
},
"dependencies": {
"@payloadcms/next": "^3.82.1",
"@payloadcms/richtext-lexical": "^3.82.1",
"@payloadcms/ui": "^3.82.1",
"@payloadcms/db-postgres": "^3.82.1",
"cross-env": "^7.0.3",
"graphql": "^16.8.1",
"next": "^16.2.3",
"payload": "^3.82.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"sharp": "^0.34.2"
},
"devDependencies": {
"@types/node": "^22.19.9",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^9.16.0",
"eslint-config-next": "^16.2.3",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
}
}

View File

@@ -0,0 +1,41 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
background-color: #fafafa;
}
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
main {
min-height: calc(100vh - 200px);
}
header {
background: white;
border-bottom: 1px solid #eee;
}
footer {
background: #f5f5f5;
padding: 2rem;
text-align: center;
color: #666;
}

View File

@@ -0,0 +1,22 @@
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: {
default: 'Next.js + Payload CMS',
template: '%s | Next.js + Payload CMS',
},
description: 'A website built with Next.js and Payload CMS',
}
export default function FrontendLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="th">
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,84 @@
import { getPayload } from 'payload'
import Link from 'next/link'
import config from '@payload-config'
export const dynamic = 'force-dynamic'
export default async function HomePage() {
let posts: any[] = []
try {
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection: 'posts',
limit: 10,
sort: '-createdAt',
})
posts = docs
} catch (e) {
// Table might not exist yet - that's OK for initial setup
console.warn('Could not fetch posts:', e)
}
return (
<main style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
<header style={{ marginBottom: '3rem', borderBottom: '1px solid #eee', paddingBottom: '1rem' }}>
<h1 style={{ fontSize: '2.5rem', marginBottom: '0.5rem' }}>
Next.js + Payload CMS
</h1>
<p style={{ color: '#666' }}>
Welcome to your new website. Edit <code>src/app/(frontend)/page.tsx</code> to customize.
</p>
</header>
<section>
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem' }}>Recent Posts</h2>
{posts.length === 0 ? (
<p style={{ color: '#888' }}>
No posts yet. Go to{' '}
<Link href="/admin" style={{ color: '#0070f3' }}>
Admin Panel
</Link>{' '}
to create your first post.
</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{posts.map((post) => (
<li
key={post.id}
style={{
padding: '1rem',
marginBottom: '1rem',
border: '1px solid #eee',
borderRadius: '8px',
}}
>
<Link
href={`/posts/${post.slug || post.id}`}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<h3 style={{ margin: 0, marginBottom: '0.5rem' }}>{post.title as string}</h3>
{post.createdAt && (
<p style={{ margin: 0, fontSize: '0.875rem', color: '#888' }}>
{new Date(post.createdAt).toLocaleDateString('th-TH', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
)}
</Link>
</li>
))}
</ul>
)}
</section>
<footer style={{ marginTop: '4rem', paddingTop: '2rem', borderTop: '1px solid #eee' }}>
<Link href="/admin" style={{ color: '#0070f3' }}>
Go to Admin Panel
</Link>
</footer>
</main>
)
}

View File

@@ -0,0 +1,23 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })
export default Page

View File

@@ -0,0 +1,2 @@
/* THIS FILE IS GENERATED BY PAYLOAD - RUN `pnpm generate:importmap` AFTER CHANGING COLLECTIONS */
export const importMap = {}

View File

@@ -0,0 +1,18 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import config from '@payload-config'
import '@payloadcms/next/css'
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const PUT = REST_PUT(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* Run `pnpm generate:importmap` to regenerate */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1 @@
/* Custom styles for Payload admin - add your overrides here */

View File

@@ -0,0 +1,30 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import config from '@payload-config'
import '@payloadcms/next/css'
import type { ServerFunctionClient } from 'payload'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -0,0 +1,16 @@
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
access: {
read: () => true,
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
],
upload: true,
}

View File

@@ -0,0 +1,70 @@
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'updatedAt'],
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
admin: {
description: 'URL-friendly version (e.g. "about-us")',
},
},
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
admin: {
description: 'Control publication status',
},
},
{
name: 'content',
type: 'richText',
label: 'Page Content',
admin: {
description: 'Main page content — use the visual editor',
},
},
{
name: 'updatedAt',
type: 'date',
admin: {
readOnly: true,
date: {
pickerAppearance: 'dayAndTime',
},
},
},
],
hooks: {
beforeChange: [
({ data }) => {
if (data.title && !data.slug) {
data.slug = data.title
.toLowerCase()
.replace(/[^a-z0-9ก-๙]+/g, '-')
.replace(/(^-|-$)/g, '')
}
return data
},
],
},
}

View File

@@ -0,0 +1,73 @@
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'createdAt'],
},
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
admin: {
description: 'URL-friendly version of the title',
},
},
{
name: 'content',
type: 'richText',
},
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'publishedAt',
type: 'date',
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
},
},
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
admin: {
description: 'Control publication status',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
if (data.title && !data.slug) {
data.slug = data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
}
return data
},
],
},
}

View File

@@ -0,0 +1,12 @@
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
],
}

View File

@@ -0,0 +1 @@
export { default as config } from './payload.config'

View File

@@ -0,0 +1,41 @@
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import sharp from 'sharp'
import { Users } from './collections/Users'
import { Media } from './collections/Media'
import { Posts } from './collections/Posts'
import { Pages } from './collections/Pages'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: Users.slug,
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [
Users,
Media,
Posts,
Pages,
],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL || '',
},
}),
sharp,
plugins: [],
})

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["./src/payload.config.ts"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,104 @@
# นโยบายความเป็นส่วนตัว
**บริษัท [ชื่อบริษัท] จำกัด** ("บริษัทฯ") ให้ความสำคัญกับการคุ้มครองข้อมูลส่วนบุคคลของท่าน จึงได้จัดทำนโยบายความเป็นส่วนตัวฉบับนี้ขึ้นเพื่อชี้แจงเกี่ยวกับการเก็บรวบรวม ใช้ หรือเปิดเผยข้อมูลส่วนบุคคล รวมถึงสิทธิต่างๆ ของท่านตามพระราชบัญญัติคุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562
## 1. ข้อมูลส่วนบุคคลที่เรา<E0B8A3>เก็บรวบรวม
ข้อมูลส่วนบุคคลที่บริษัทฯ อาจเก็บรวบรวมจากท่าน อาจรวมถึง:
- **ข้อมูลส่วนบุคคลทั่วไป:** ชื่อ-นามสกุล ที่อยู่อีเมล หมายเลขโทรศัพท์ ที่อยู่ไปรษณีย์ วันเดือนปีเกิด
- **ข้อมูลการติดต่อ:** ข้อมูลการติดต่อที่ท่านให้ไว้เมื่อลงทะเบียน กรอกแบบฟอร์ม หรือติดต่อบริษัทฯ
- **ข้อมูลการใช้งาน:** IP address, ข้อมูลการเข้าชมเว็บไซต์, คุกกี้, ข้อมูลกิจกรรมการใช้งาน
- **ข้อมูลการเงิน:** ข้อมูลบัตรเครดิต/เดบิต ข้อมูลการชำระเงิน (กรณีใช้บริการที่มีค่าใช้จ่าย)
- **ข้อมูลอื่นที่ท่านให้ไว้:** ข้อมูลใดๆ ที่ท่านให้ไว้โดยสมัครใจผ่านช่องทางการติดต่อของบริษัทฯ
## 2. วัตถุประสงค์ในการเก็บรวบรวมข้อมูล
บริษัทฯ เก็บรวบรวมข้อมูลส่วนบุคคลเพื่อวัตถุประสงค์ดังต่อไปนี้:
- เพื่อให้บริการและดำเนินการตามคำขอของท่าน
- เพื่อการสื่อสารและให้ข้อมูลข่าวสารเกี่ยวกับบริการของบริษัทฯ
- เพื่อปรับปรุงคุณภาพบริการและพัฒนาเว็บไซต์
- เพื่อการวิเคราะห์ข้อมูลและสถิติการใช้งาน
- เพื่อการตลาดและการโฆษณา (ได้รับความยินยอมจากท่าน)
- เพื่อการปฏิบัติตามกฎหมาย คำสั่ง หรือกระบวนการทางกฎหมาย
- เพื่อการรักษาความปลอดภัยและป้องกันการทุจริต
## 3. การใช้และเปิดเผยข้อมูลส่วนบุคคล
บริษัทฯ จะไม่เปิดเผยข้อมูลส่วนบุคคลของท่านต่อบุคคลที่สาม เว้นแต่:
- ได้รับความยินยอมจากท่าน
- จำเป็นต้องเปิดเผยเพื่อให้บริการตามคำขอของท่าน
- จำเป็นต้องเปิดเผยต่อผู้ให้บริการที่บริษัทฯ ว่าจ้างให้ดำเนินการในส่วนที่จำเป็น
- กฎหมายกำหนดหรือร้ณขอให้เปิดเผย
- เพื่อป้องกันหรือระงับอันตรายต่อชีวิต สุขภาพ หรือทรัพย์สิน
- จำเป็นเพื่อประโยชน์โดยชอบด้วยกฎหมายของบริษัทฯ
## 4. ระยะเวลาการเก็บรักษาข้อมูล
บริษัทฯ จะเก็บรักษาข้อมูลส่วนบุคคลของท่านตราบเท่าที่จำเป็นเพื่อบรรลุวัตถุประสงค์ที่ระบุไว้ในนโยบายนี้ เว้นแต่กฎหมายกำหนดให้เก็บรักษาไว้นานกว่านั้น
ท่านสามารถขอให้บริษัทฯ ลบหรือทำลายข้อมูลส่วนบุคคลของท่านได้ตามสิทธิ์ของท่านในข้อ 7
## 5. การคุ้มครองข้อมูลส่วนบุคคล
บริษัทฯ มีมาตรการรักษาความปลอดภัยที่เหมาะสมเพื่อป้องกันการสูญหาย เข้าถึง ใช้ เปลี่ยนแปลง แก้ไข หรือเปิดเผยข้อมูลส่วนบุคคลโดยไม่ได้รับอนุญาต รวมถึงมาตรการทางเทคนิคและองค์กรที่จำเป็น
## 6. การใช้คุกกี้
เว็บไซต์ของบริษัทฯ อาจใช้คุกกี้และเทคโนโลยีที่คล้ายคลึงกันเพื่อ:
- จดจำการตั้งค่าของท่าน
- วิเคราะห์การใช้งานเว็บไซต์
- ปรับปรุงประสบการณ์การใช้งาน
- แสดงเนื้อหาและโฆษณาที่ท่านสนใจ
ท่านสามารถตั้งค่าเบราว์เซอร์ของท่านเพื่อปฏิเสธคุกกี้บางประเภทหรือทั้งหมดได้ แต่การปฏิเสธคุกกี้อาจส่งผลต่อการทำงานของเว็บไซต์
## 7. สิทธิของเจ้าของข้อมูล
ตามพระราชบัญญัติคุ้มครองข้อมูลส่วนบุคคล ท่านมีสิทธิดังต่อไปนี้:
**7.1 สิทธิในการเข้าถึง**
ท่านมีสิทธิขอเข้าถึงและขอรับสำเนาข้อมูลส่วนบุคคลของท่านที่บริษัทฯ มีอยู่
**7.2 สิทธิในการแก้ไข**
ท่านมีสิทธิขอให้บริษัทฯ แก้ไขข้อมูลส่วนบุคคลที่ไม่ถูกต้องหรือไม่สมบูรณ์
**7.3 สิทธิในการลบ**
ท่านมีสิทธิขอให้บริษัทฯ ลบข้อมูลส่วนบุคคลของท่าน ในกรณีที่ข้อมูลนั้นไม่จำเป็นต้องเก็บรักษาไว้ต่อไป
**7.4 สิทธิในการระงับการใช้**
ท่านมีสิทธิขอให้บริษัทฯ ระงับการใช้ข้อมูลส่วนบุคคลของท่านในบางกรณี
**7.5 สิทธิในการคัดค้าน**
ท่านมีสิทธิคัดค้านการเก็บรวบรวม ใช้ หรือเปิดเผยข้อมูลส่วนบุคคลของท่าน
**7.6 สิทธิในการโอนย้าย**
ท่านมีสิทธิขอรับข้อมูลส่วนบุคคลในรูปแบบที่สามารถอ่านได้ด้วยเครื่องมือหรืออุปกรณ์อัตโนมัติ และขอส่งหรือโอนข้อมูลนั้นไปยังระบบอื่น
**7.7 สิทธิในการถอนความยินยอม**
ท่านมีสิทธิถอนความยินยอมที่ท่านได้ให้ไว้แก่บริษัทฯ ได้ตลอดเวลา
**7.8 สิทธิในการร้องเรียน**
ท่านมีสิทธิร้องเรียนต่อหน่วยงานกำกับดูแล (สำนักงานคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล) หากบริษัทฯ ละเมิดหรือไม่ปฏิบัติตามพระราชบัญญัติคุ้มครองข้อมูลส่วนบุคคล
## 8. การติดต่อบริษัทฯ
หากท่านมีคำถาม ข้อสงสัย หรือต้องการใช้สิทธิใดๆ ตามนโยบายนี้ กรุณาติดต่อบริษัทฯ:
**บริษัท [ชื่อบริษัท] จำกัด**
**ที่อยู่:** [ที่อยู่บริษัท]
**โทรศัพท์:** [หมายเลขโทรศัพท์]
**อีเมล:** [อีเมลติดต่อ]
## 9. การเปลี่ยนแปลงนโยบาย
บริษัทฯ อาจปรับปรุงหรือเปลี่ยนแปลงนโยบายความเป็นส่วนตัวนี้เป็นครั้งคราว การเปลี่ยนแปลงจะมีผลเมื่อประกาศบนเว็บไซต์ ท่านควรตรวจสอบนโยบายนี้เป็นระยะเพื่อรับทราบการเปลี่ยนแปลง
**วันที่มีผลบังคับใช้:** [วันที่เริ่มใช้]
---
*นโยบายความเป็นส่วนตัวฉบับนี้จัดทำขึ้นตามพระราชบัญญัติคุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562*

View File

@@ -0,0 +1,371 @@
# เงื่อนไขการให้บริการ (Terms of Service)
**ชื่อเว็บไซต์:** {SITE_NAME}
**เว็บไซต์:** {SITE_URL}
**มีผลบังคับใช้วันที่:** {EFFECTIVE_DATE}
**แก้ไขล่าสุด:** {LAST_UPDATED}
## 1. การยอมรับเงื่อนไข
### 1.1 ข้อตกลง
ด้วยการเข้าถึงและใช้งานเว็บไซต์ {SITE_URL} ("เว็บไซต์") ของบริษัท {COMPANY_NAME} ("เรา", "ของเรา" หรือ "บริษัท") ท่าน ("ผู้ใช้", "ท่าน" หรือ "ของคุณ") ยอมรับและตกลงที่จะถูกผูกมัดด้วยเงื่อนไขการให้บริการฉบับนี้ ("เงื่อนไข")
### 1.2 การแก้ไขเงื่อนไข
เราขอสงวนสิทธิในการแก้ไขเงื่อนไขนี้เมื่อใดก็ได้:
- การแก้ไขจะมีผลทันทีเมื่อโพสต์บนเว็บไซต์
- ท่านควรตรวจสอบเงื่อนไขนี้เป็นประจำ
- การใช้งานเว็บไซต์ต่อเนื่องแสดงว่าท่านยอมรับการแก้ไข
### 1.3 อายุขั้นต่ำ
ท่านต้องมีอายุไม่ต่ำกว่า 20 ปีบริบูรณ์เพื่อใช้งานเว็บไซต์:
- หากท่านอายุต่ำกว่า 20 ปี ท่านต้องได้รับความยินยอมจากผู้ปกครอง
- ผู้ปกครองต้องตกลงที่จะผูกพันด้วยเงื่อนไขนี้
## 2. บริการของเรา
### 2.1 คำอธิบายบริการ
{BUSINESS_NAME} ให้บริการ:
1. **{SERVICE_1}** - {SERVICE_1_DESC}
2. **{SERVICE_2}** - {SERVICE_2_DESC}
3. **{SERVICE_3}** - {SERVICE_3_DESC}
### 2.2 การเปลี่ยนแปลงบริการ
เราขอสงวนสิทธิในการ:
- เพิ่ม ลบ หรือแก้ไขฟีเจอร์ของบริการ
- ระงับหรือยุติบริการชั่วคราวหรือถาวร
- จำกัดการเข้าถึงบางส่วนหรือทั้งหมดของบริการ
### 2.3 ความพร้อมของบริการ
เราพยายามให้บริการอย่างต่อเนื่อง แต่:
- เราไม่รับประกันว่าบริการจะปราศจากข้อผิดพลาด
- เราไม่รับผิดชอบต่อ downtime ที่ไม่ได้ตั้งใจ
- เราขอสงวนสิทธิในการหยุดให้บริการโดยไม่แจ้งล่วงหน้า
## 3. บัญชีผู้ใช้
### 3.1 การสร้างบัญชี
เพื่อใช้งานบริการบางอย่าง ท่านต้องสร้างบัญชีผู้ใช้:
- ท่านต้องให้ข้อมูลที่ถูกต้อง ครบถ้วน และทันสมัย
- ท่านต้องรักษารหัสผ่านให้เป็นความลับ
- ท่านรับผิดชอบต่อทุกกิจกรรมที่เกิดขึ้นภายใต้บัญชีของท่าน
### 3.2 ข้อกำหนดของบัญชี
- หนึ่งคนต่อหนึ่งบัญชีเท่านั้น
- ห้ามแบ่งปันบัญชีกับผู้อื่น
- ห้ามใช้ชื่อบัญชีที่ผิดกฎหมายหรือละเมิดสิทธิผู้อื่น
### 3.3 การระงับบัญชี
เราขอสงวนสิทธิในการระงับหรือลบบัญชีของท่านหาก:
- ท่านละเมิดเงื่อนไขนี้
- มีกิจกรรมที่น่าสงสัยหรือเป็นอันตราย
- มีการร้องเรียนจากผู้ใช้รายอื่น
- ตามข้อกำหนดของกฎหมาย
### 3.4 การลบบัญชี
ท่านสามารถลบบัญชีของท่านเมื่อใดก็ได้:
- ติดต่อเราที่ {CONTACT_EMAIL}
- ข้อมูลบางอย่างอาจถูกเก็บไว้ตามข้อกำหนดของกฎหมาย
- การลบบัญชีไม่สามารถย้อนกลับได้
## 4. ความเป็นเจ้าของทรัพย์สินทางปัญญา
### 4.1 สิทธิของเรา
เว็บไซต์และเนื้อหาทั้งหมดเป็นทรัพย์สินของเราหรือผู้ให้ใบอนุญาต:
- เนื้อหา ข้อความ กราฟิก โลโก้
- ซอฟต์แวร์ โค้ด ฐานข้อมูล
- การออกแบบ เลย์เอาต์
### 4.2 เครื่องหมายการค้า
เครื่องหมายการค้า โลโก้ และชื่อบริการเป็นเครื่องหมายการค้าของเรา:
- ห้ามใช้โดยไม่ได้รับอนุญาตเป็นลายลักษณ์อักษร
- การใช้โดยไม่ได้รับอนุญาตอาจเป็นการละเมิดกฎหมาย
### 4.3 สิทธิของท่าน
ท่านยังคงเป็นเจ้าของเนื้อหาที่ท่านส่งมา:
- ท่านยังคงเป็นเจ้าของเนื้อหาของท่าน
- ท่านให้ใบอนุญาตแก่เราในการใช้เนื้อหานั้น
- ท่านรับประกันว่ามีสิทธิในการให้ใบอนุญาต
### 4.4 ใบอนุญาตการใช้งาน
ท่านได้รับใบอนุญาตที่เพิกถอนได้ ไม่เฉพาะเจาะจง ไม่สามารถโอนย้ายได้:
- เข้าถึงและใช้งานบริการเพื่อวัตถุประสงค์ส่วนบุคคล
- ห้ามใช้เพื่อวัตถุประสงค์เชิงพาณิชย์โดยไม่ได้รับอนุญาต
- ห้ามดัดแปลง แก้ไข หรือสร้างงานดัดแปลง
## 5. ข้อห้ามในการใช้งาน
### 5.1 กิจกรรมที่ต้องห้าม
ท่านตกลงที่จะไม่:
**กิจกรรมที่ผิดกฎหมาย:**
- ใช้เว็บไซต์เพื่อกิจกรรมที่ผิดกฎหมาย
- ละเมิดสิทธิทางปัญญาของผู้อื่น
- ละเมิดความเป็นส่วนตัวของผู้อื่น
- ส่งเนื้อหาที่ผิดกฎหมายหรือเป็นอันตราย
**กิจกรรมที่เป็นอันตราย:**
- เผยแพร่ไวรัส มัลแวร์ หรือโค้ดที่เป็นอันตราย
- พยายามเข้าถึงระบบโดยไม่ได้รับอนุญาต
- รบกวนหรือขัดขวางการทำงานของเว็บไซต์
- ดำเนินการ reverse engineering ของซอฟต์แวร์
**กิจกรรมที่ละเมิดสิทธิ:**
- ละเมิดลิขสิทธิ์ เครื่องหมายการค้า หรือสิทธิอื่นๆ
- ใช้ข้อมูลส่วนบุคคลของผู้อื่นโดยไม่ได้รับอนุญาต
- ส่งสแปมหรือข้อความเชิงพาณิชย์ที่ไม่พึงประสงค์
- ปลอมแปลงตัวตนหรือแหล่งที่มาของเนื้อหา
### 5.2 ผลของการละเมิด
หากท่านละเมิดข้อห้าม:
- บัญชีของท่านอาจถูกระงับหรือลบ
- เราอาจดำเนินการทางกฎหมาย
- เราอาจแจ้งหน่วยงานบังคับใช้กฎหมาย
## 6. เนื้อหาที่ผู้ใช้ส่ง
### 6.1 คำจำกัดความ
"เนื้อหาที่ผู้ใช้ส่ง" หมายถึงเนื้อหาใดๆ ที่ท่านส่ง โพสต์ หรือแสดงบนเว็บไซต์:
- ความคิดเห็น รีวิว
- รูปภาพ วิดีโอ
- ข้อความ ไฟล์
### 6.2 ใบอนุญาต
โดยส่งเนื้อหา ท่านให้ใบอนุญาตแก่เรา:
- ใบอนุญาตทั่วโลก ไม่เฉพาะเจาะจง ย่อยได้
- สิทธิในการใช้ ทำซ้ำ ดัดแปลง เผยแพร่
- สิทธิในการแสดงเนื้อหา
- ใบอนุญาตนี้ไม่มีค่าตอบแทน
### 6.3 ความรับผิดชอบของท่าน
ท่านรับผิดชอบเนื้อหาที่ท่านส่ง:
- ท่านรับประกันว่ามีสิทธิในการส่งเนื้อหา
- เนื้อหาไม่ละเมิดสิทธิของผู้อื่น
- เนื้อหาไม่ผิดกฎหมายหรือเป็นอันตราย
### 6.4 การตรวจสอบเนื้อหา
เราขอสงวนสิทธิในการ:
- ตรวจสอบเนื้อหาที่ส่งมา
- ลบเนื้อหาที่ละเมิดเงื่อนไข
- รายงานกิจกรรมที่ผิดกฎหมายต่อเจ้าหน้าที่
## 7. การชำระเงิน
### 7.1 ราคาและค่าธรรมเนียม
- ราคาทั้งหมดแสดงเป็นเงินบาทไทย (THB)
- ราคานี้ {PRICE_INCLUDES_VAT}
- เราขอสงวนสิทธิในการเปลี่ยนราคาเมื่อใดก็ได้
### 7.2 การชำระเงิน
การชำระเงินต้องชำระล่วงหน้า:
- เรายอมรับการชำระเงินผ่าน {PAYMENT_METHODS}
- การชำระเงินจะประมวลผลโดยบุคคลที่สาม
- ท่านต้องให้ข้อมูลการชำระเงินที่ถูกต้อง
### 7.3 การคืนเงิน
นโยบายการคืนเงิน:
- ผู้ใช้สามารถขอคืนเงินได้ภายใน {REFUND_DAYS} วันนับจากวันที่ชำระเงิน
- การคืนเงินจะดำเนินการภายใน {REFUND_PROCESSING_DAYS} วันทำการ
### 7.4 การต่ออายุอัตโนมัติ
หากบริการมีการต่ออายุอัตโนมัติ:
- ท่านจะได้รับแจ้งก่อนการต่ออายุ
- ท่านสามารถยกเลิกการต่ออายุเมื่อใดก็ได้
- การยกเลิกจะมีผลหลังระยะเวลาปัจจุบันสิ้นสุด
## 8. การปฏิเสธความรับผิดชอบ
### 8.1 "ตามที่เป็น"
บริการให้บริการ "ตามที่เป็น" และ "ตามที่มี":
- เราไม่รับประกันว่าบริการจะปราศจากข้อผิดพลาด
- เราไม่รับประกันว่าบริการจะตรงตามความต้องการของท่าน
- เราไม่รับประกันความถูกต้องของข้อมูล
### 8.2 การปฏิเสธความรับผิดชอบ
ภายใต้ขอบเขตที่กฎหมายอนุญาต เราปฏิเสธความรับผิดชอบ:
- ความเสียหายโดยตรง ทางอ้อม โดยบังเอิญ หรือเชิงลงโทษ
- การสูญเสียข้อมูลหรือข้อมูล
- การหยุดชะงักของธุรกิจ
- ความเสียหายอื่นๆ
### 8.3 ข้อจำกัดความรับผิด
ความรับผิดรวมของเราจะไม่เกิน:
- จำนวนที่ท่านจ่ายให้เราในช่วง 12 เดือนที่ผ่านมา
- หรือ {LIABILITY_MAX_AMOUNT} บาท แล้วแต่จำนวนใดมากกว่า
### 8.4 ข้อยกเว้น
ข้อจำกัดบางอย่างไม่ใช่บังคับกับ:
- การเสียชีวิตหรือการบาดเจ็บส่วนบุคคล
- การฉ้อโกงหรือการแสดงโดยประมาทเลินเล่ออย่างร้ายแรง
- หน้าที่ที่ไม่สามารถถูกจำกัดตามกฎหมาย
## 9. การชดเชย
### 9.1 ข้อตกลงการชดเชย
ท่านตกลงที่จะชดใช้และปกป้องเราจาก:
- การเรียกร้อง ค่าเสียหาย ค่าใช้จ่าย
- ที่เกิดจากการใช้งานเว็บไซต์ของท่าน
- ที่เกิดจากการละเมิดเงื่อนไขนี้
- ที่เกิดจากการละเมิดสิทธิของผู้อื่น
### 9.2 ขั้นตอนการชดเชย
เมื่อได้รับการเรียกร้อง:
- เราจะแจ้งท่านเป็นลายลักษณ์อักษร
- ท่านจะมีสิทธิในการป้องกัน
- เราจะร่วมมือในการป้องกัน
## 10. ความเป็นส่วนตัว
### 10.1 นโยบายความเป็นส่วนตัว
การใช้ข้อมูลส่วนบุคคลอยู่ภายใต้นโยบายความเป็นส่วนตัว:
- อ่านนโยบายความเป็นส่วนตัวของเราที่ {PRIVACY_POLICY_URL}
- นโยบายความเป็นส่วนตัวเป็นส่วนหนึ่งของเงื่อนไขนี้
- ในกรณีที่มีความขัดแย้ง เงื่อนไขนี้จะมีผลบังคับใช้
### 10.2 Cookie
เราใช้ Cookie และเทคโนโลยีการติดตาม:
- อ่านนโยบาย Cookie ของเรา
- ท่านสามารถจัดการการตั้งค่า Cookie ได้
- การปิดการใช้งาน Cookie อาจจำกัดการทำงานของเว็บไซต์
## 11. การยุติบริการ
### 11.1 การยุติโดยท่าน
ท่านสามารถยุติการใช้งานเว็บไซต์เมื่อใดก็ได้:
- หยุดใช้งานเว็บไซต์
- ลบบัญชีของท่าน
- ส่งคำขอเป็นลายลักษณ์อักษรที่ {CONTACT_EMAIL}
### 11.2 การยุติโดยเรา
เราขอสงวนสิทธิในการยุติการเข้าถึงของท่าน:
- โดยไม่แจ้งล่วงหน้า
- ด้วยเหตุผลใดๆ หรือไม่มีเหตุผล
- ทันทีที่มีผล
### 11.3 ผลของการยุติ
เมื่อการเข้าถึงถูกยุติ:
- สิทธิ์ในการใช้งานเว็บไซต์สิ้นสุดลง
- ท่านต้องหยุดใช้งานเว็บไซต์ทันที
- ข้อกำหนดบางประการยังคงมีผล
## 12. กฎหมายที่ใช้บังคับ
### 12.1 กฎหมายไทย
เงื่อนไขนี้ถูกควบคุมและตีความตามกฎหมายแห่งราชอาณาจักรไทย:
- พระราชบัญญัติคุ้มครองผู้บริโภค
- พระราชบัญญัติว่าด้วยการกระทำความผิดเกี่ยวกับคอมพิวเตอร์
- พระราชบัญญัติลิขสิทธิ์
- กฎหมายที่เกี่ยวข้องอื่นๆ
### 12.2 เขตอำนาจศาล
ข้อพิพาทใดๆ อยู่ภายใต้เขตอำนาจศาลของ:
- ศาลแพ่ง/ศาลล้มละลายกลาง {COURT_LOCATION}
- หรือศาลที่มีเขตอำนาจในประเทศไทย
### 12.3 การระงับข้อพิพาท
ก่อนดำเนินการทางกฎหมาย:
- พยายามเจรจาเพื่อระงับข้อพิพาท
- ใช้เวลา {NEGOTIATION_DAYS} วันในการเจรจา
- หากไม่สำเร็จ จึงดำเนินการทางกฎหมาย
## 13. การติดต่อ
หากท่านมีคำถามเกี่ยวกับเงื่อนไขนี้:
**อีเมล:** {CONTACT_EMAIL}
**โทรศัพท์:** {CONTACT_PHONE}
**ที่อยู่:** {COMPANY_ADDRESS}
---
## ภาคผนวก: คำจำกัดความ
**"บัญชี"** หมายถึง บัญชีผู้ใช้ที่ท่านสร้างบนเว็บไซต์
**"เนื้อหา"** หมายถึง ข้อมูล ข้อความ กราฟิก ภาพ วิดีโอ ซอฟต์แวร์ หรือวัสดุอื่นๆ
**"เว็บไซต์"** หมายถึง เว็บไซต์ {SITE_URL} และบริการที่เกี่ยวข้องทั้งหมด
**"เรา" "ของเรา"** หมายถึง บริษัท {COMPANY_NAME}
**"ท่าน" "ผู้ใช้"** หมายถึง บุคคลหรือนิติบุคคลที่เข้าถึงหรือใช้งานเว็บไซต์
---
**ลงชื่อ:** _________________________
**ชื่อ:** {AUTHORIZED_NAME}
**ตำแหน่ง:** {AUTHORIZED_TITLE}
**วันที่:** {SIGN_DATE}
**บริษัท {COMPANY_NAME}**
---
*เอกสารนี้เป็นเอกสารทางกฎหมาย หากท่านมีข้อสงสัย กรุณาปรึกษาที่ปรึกษากฎหมาย*
---
## Placeholders ที่ต้องแทนที่
| Placeholder | คำอธิบาย |
|-------------|----------|
| {SITE_NAME} | ชื่อเว็บไซต์ |
| {SITE_URL} | URL เว็บไซต์ |
| {COMPANY_NAME} | ชื่อบริษัท |
| {BUSINESS_NAME} | ชื่อธุรกิจ |
| {EFFECTIVE_DATE} | วันที่มีผลบังคับใช้ |
| {LAST_UPDATED} | วันที่แก้ไขล่าสุด |
| {SERVICE_1}, {SERVICE_2}, {SERVICE_3} | ชื่อบริการ |
| {SERVICE_1_DESC}, {SERVICE_2_DESC}, {SERVICE_3_DESC} | คำอธิบายบริการ |
| {CONTACT_EMAIL} | อีเมลติดต่อ |
| {CONTACT_PHONE} | โทรศัพท์ติดต่อ |
| {COMPANY_ADDRESS} | ที่อยู่บริษัท |
| {PRIVACY_POLICY_URL} | URL นโยบายความเป็นส่วนตัว |
| {PRICE_INCLUDES_VAT} | ราคา<E0B884>รวม VAT หรือไม่ |
| {PAYMENT_METHODS} | วิธีการชำระเงิน |
| {REFUND_DAYS} | วันในการขอคืนเงิน |
| {REFUND_PROCESSING_DAYS} | วันในการดำเนินการคืนเงิน |
| {LIABILITY_MAX_AMOUNT} | จำนวนความรับผิดสูงสุด |
| {COURT_LOCATION} | สถานที่ตั้งศาล |
| {NEGOTIATION_DAYS} | วันในการเจรจา |
| {AUTHORIZED_NAME} | ชื่อผู้ลงนาม |
| {AUTHORIZED_TITLE} | ตำแหน่งผู้ลงนาม |
| {SIGN_DATE} | วันที่ลงนาม |