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
317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
'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>
|
|
)
|
|
}
|