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:
69
skills/website-creator/templates/collections/Users.ts
Normal file
69
skills/website-creator/templates/collections/Users.ts
Normal 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
|
||||
],
|
||||
}
|
||||
44
skills/website-creator/templates/collections/access/index.ts
Normal file
44
skills/website-creator/templates/collections/access/index.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
462
skills/website-creator/templates/consent/CookieConsent.astro
Normal file
462
skills/website-creator/templates/consent/CookieConsent.astro
Normal 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>
|
||||
99
skills/website-creator/templates/consent/README.md
Normal file
99
skills/website-creator/templates/consent/README.md
Normal 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
|
||||
81
skills/website-creator/templates/consent/api/consent.ts
Normal file
81
skills/website-creator/templates/consent/api/consent.ts
Normal 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' },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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' } }
|
||||
)
|
||||
}
|
||||
}
|
||||
80
skills/website-creator/templates/consent/api/route.ts
Normal file
80
skills/website-creator/templates/consent/api/route.ts
Normal 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'
|
||||
})
|
||||
}
|
||||
@@ -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 หรือไม่',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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
|
||||
316
skills/website-creator/templates/consent/cookie-banner.tsx
Normal file
316
skills/website-creator/templates/consent/cookie-banner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
dist
|
||||
build
|
||||
*.log
|
||||
.env*.local
|
||||
.DS_Store
|
||||
*.pem
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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:
|
||||
@@ -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 })
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
/* THIS FILE IS GENERATED BY PAYLOAD - RUN `pnpm generate:importmap` AFTER CHANGING COLLECTIONS */
|
||||
export const importMap = {}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
/* Custom styles for Payload admin - add your overrides here */
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as config } from './payload.config'
|
||||
@@ -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: [],
|
||||
})
|
||||
@@ -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"]
|
||||
}
|
||||
104
skills/website-creator/templates/privacy-policy.md
Normal file
104
skills/website-creator/templates/privacy-policy.md
Normal 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*
|
||||
371
skills/website-creator/templates/terms-of-service.md
Normal file
371
skills/website-creator/templates/terms-of-service.md
Normal 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} | วันที่ลงนาม |
|
||||
Reference in New Issue
Block a user