feat: Import 35+ skills, merge duplicates, add openclaw installer

Major updates:
- Added 35+ new skills from awesome-opencode-skills and antigravity repos
- Merged SEO skills into seo-master
- Merged architecture skills into architecture
- Merged security skills into security-auditor and security-coder
- Merged testing skills into testing-master and testing-patterns
- Merged pentesting skills into pentesting
- Renamed website-creator to thai-frontend-dev
- Replaced skill-creator with github version
- Removed Chutes references (use MiniMax API instead)
- Added install-openclaw-skills.sh for cross-platform installation
- Updated .env.example with MiniMax API credentials
This commit is contained in:
Kunthawat Greethong
2026-03-26 11:37:39 +07:00
parent 48595100a1
commit 7edf5bc4d0
469 changed files with 131580 additions and 417 deletions

View File

@@ -1,19 +0,0 @@
# Website Configuration
# Fill these after generating your website
# Umami Analytics (Optional - Self-hosted)
# Get from: Your Umami dashboard → Settings → Websites
UMAMI_WEBSITE_ID=
UMAMI_DOMAIN=analytics.example.com
# Admin Dashboard
# Change this before deploying to production!
ADMIN_PASSWORD=(auto-generated from folder name)
# Database (Optional - for production with Turso)
# ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
# ASTRO_DB_APP_TOKEN=your-turso-token
# Site Configuration
SITE_URL=https://your-domain.com
SITE_NAME="Your Website Name"

View File

@@ -1,776 +0,0 @@
#!/usr/bin/env python3
"""
Website Creator - Generate PDPA-compliant Astro websites
Creates complete Astro projects with:
- Bilingual support (Thai/English)
- Umami Analytics integration (auto-create)
- GA4 Analytics support (existing or new)
- Google Search Console setup
- Cookie consent management
- Consent logging database (Astro DB)
- PDPA-compliant legal pages
- Easypanel deployment (manual sync after local preview)
Usage:
python3 create_astro_website.py \\
--name "Deal Plus Tech" \\
--type "corporate" \\
--languages "th,en" \\
--output "./dealplustech-website"
# Then preview locally, and when ready:
# Script will ask: "Sync to Gitea and deploy?"
"""
import os
import sys
import argparse
import shutil
import subprocess
import json
import time
from pathlib import Path
from datetime import datetime
from urllib.parse import urlparse
# ============================================================================
# INTERACTIVE SETUP FUNCTIONS
# ============================================================================
def ask_analytics_setup():
"""
Interactive analytics setup workflow
Returns:
dict: Analytics configuration
"""
print("\n" + "=" * 60)
print("📊 ANALYTICS SETUP")
print("=" * 60)
config = {
"search_console": None,
"analytics_type": None, # 'umami' or 'ga4'
"umami_auto_create": False,
"umami_website_id": None,
"ga4_property_id": None,
"ga4_credentials_path": None,
"ga4_existing": False,
}
# Step 1: Google Search Console (for all websites)
print("\n1⃣ Google Search Console Setup")
print(" GSC is recommended for all websites for SEO monitoring.")
gsc_choice = (
input("\n Do you want to setup Google Search Console? (y/n): ")
.strip()
.lower()
)
if gsc_choice == "y":
print("\n GSC Setup Options:")
print(" 1. I'll add it manually later (skip for now)")
print(" 2. I have service account credentials file")
gsc_method = input("\n Choose option (1-2): ").strip()
if gsc_method == "2":
gsc_path = input(" Enter path to GSC credentials file: ").strip()
if os.path.exists(gsc_path):
config["search_console"] = {
"credentials_path": gsc_path,
"setup_later": False,
}
print(" ✓ GSC credentials loaded")
else:
print(" ⚠ File not found, will setup later")
config["search_console"] = {"setup_later": True}
else:
config["search_console"] = {"setup_later": True}
print(" ✓ Will setup later")
else:
print(" ⏭️ Skipping GSC setup")
# Step 2: Choose Analytics Type (Umami OR GA4)
print("\n2⃣ Analytics Platform")
print(" Choose ONE analytics platform:")
print(" 1. Umami Analytics (recommended for most users)")
print(" - Privacy-focused, self-hosted")
print(" - Simple setup, auto-created")
print(" - Good for most websites")
print("\n 2. Google Analytics 4 (for advanced users)")
print(" - Full-featured analytics")
print(" - Requires Google account")
print(" - Good for existing GA4 users")
analytics_choice = input("\n Choose analytics (1-2): ").strip()
if analytics_choice == "1":
# Umami setup
config["analytics_type"] = "umami"
print("\n 📈 Umami Analytics Setup")
# Check if Umami credentials are configured
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "../../../.env"))
umami_url = os.getenv("UMAMI_URL", "")
umami_username = os.getenv("UMAMI_USERNAME", "")
umami_password = os.getenv("UMAMI_PASSWORD", "")
if umami_url and umami_username and umami_password:
print(" ✓ Umami credentials found in .env")
print(" ✓ Will auto-create Umami website for this project")
config["umami_auto_create"] = True
else:
print(" ⚠ Umami credentials not configured in .env")
print(" ⏭️ Skipping Umami setup (can add manually later)")
elif analytics_choice == "2":
# GA4 setup
config["analytics_type"] = "ga4"
print("\n 🔍 Google Analytics 4 Setup")
print(" 1. Create new GA4 property (auto-setup)")
print(" 2. Use existing GA4 property (manual setup)")
ga4_choice = input("\n Choose option (1-2): ").strip()
if ga4_choice == "1":
print("\n ⚠ Auto-creating GA4 properties requires API setup.")
print(" ⏭️ Will provide instructions for manual setup")
config["ga4_existing"] = False
else:
print("\n Please provide your existing GA4 details:")
# Check unified .env for GA4 credentials
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "../../../.env"))
ga4_property_id = os.getenv("GA4_PROPERTY_ID", "")
ga4_credentials_path = os.getenv("GA4_CREDENTIALS_PATH", "")
if ga4_property_id:
print(f" Found GA4 Property ID in .env: {ga4_property_id[:20]}...")
use_global = (
input(" Use this for this project? (y/n): ").strip().lower()
)
if use_global == "y":
config["ga4_property_id"] = ga4_property_id
config["ga4_credentials_path"] = ga4_credentials_path
print(" ✓ Using global GA4 credentials")
else:
config["ga4_property_id"] = input(
" Enter GA4 Property ID: "
).strip()
config["ga4_credentials_path"] = input(
" Enter GA4 credentials file path: "
).strip()
else:
config["ga4_property_id"] = input(
" Enter GA4 Property ID (G-XXXXXXXXXX): "
).strip()
config["ga4_credentials_path"] = input(
" Enter GA4 credentials file path: "
).strip()
config["ga4_existing"] = True
else:
print(" ⏭️ Skipping analytics setup")
return config
# ============================================================================
# TEMPLATES (abbreviated for brevity)
# ============================================================================
ASTRO_CONFIG_TEMPLATE = """import {{ defineConfig }} from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import db from '@astrojs/db';
import sitemap from '@astrojs/sitemap';
export default defineConfig({{
site: '{site_url}',
output: 'hybrid',
i18n: {{
locales: [{locales}],
defaultLocale: '{default_locale}',
routing: {{
prefixDefaultLocale: false,
fallbackType: 'rewrite',
}},
fallback: {{
th: 'en',
}},
}},
integrations: [
tailwindcss(),
db(),
sitemap({{
i18n: {{
defaultLocale: '{default_locale}',
}},
}}),
],
}});
"""
PACKAGE_JSON_TEMPLATE = """{{
"name": "{name}",
"type": "module",
"version": "1.0.0",
"scripts": {{
"dev": "astro dev",
"build": "astro build --remote",
"preview": "astro preview",
"astro": "astro",
"db:push": "astro db push --remote",
"db:seed": "astro db seed"
}},
"dependencies": {{
"astro": "^5.17.1",
"@astrojs/db": "^0.14.0",
"@astrojs/sitemap": "^3.2.0",
"@tailwindcss/vite": "^4.2.1",
"tailwindcss": "^4.2.1",
"astro-consent": "^1.0.0",
"drizzle-orm": "^0.38.0",
"@libsql/client": "^0.14.0"
}}
}}
"""
# ... (rest of templates remain the same)
# ============================================================================
# MAIN FUNCTION
# ============================================================================
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Create PDPA-compliant Astro website")
parser.add_argument("--name", required=True, help="Website name")
parser.add_argument(
"--type",
default="corporate",
choices=["corporate", "portfolio", "landing", "blog", "ecommerce"],
help="Website type",
)
parser.add_argument(
"--languages", default="th,en", help="Languages (comma-separated): th, en"
)
parser.add_argument(
"--primary-color", default="#2563eb", help="Primary color (hex)"
)
parser.add_argument(
"--secondary-color", default="#1e40af", help="Secondary color (hex)"
)
parser.add_argument(
"--features",
default="blog,contact",
help="Features (comma-separated): blog, products, contact, portfolio",
)
parser.add_argument("--umami-id", default="", help="Umami Website ID")
parser.add_argument(
"--umami-domain", default="analytics.example.com", help="Umami domain"
)
parser.add_argument("--output", "-o", default=".", help="Output directory")
parser.add_argument(
"--no-interactive",
action="store_true",
help="Skip interactive setup (use defaults)",
)
args = parser.parse_args()
# Auto-generate admin password from project folder name
args.admin_password = Path(args.output).name.replace(" ", "").lower()
# Load unified credentials
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "../../../.env"))
# Get Umami credentials for auto-setup
args.umami_url = os.getenv("UMAMI_URL", "")
args.umami_username = os.getenv("UMAMI_USERNAME", "")
args.umami_password = os.getenv("UMAMI_PASSWORD", "")
args.auto_setup_umami = bool(
args.umami_url and args.umami_username and args.umami_password
)
languages = [lang.strip() for lang in args.languages.split(",")]
default_locale = "en" if "en" in languages else languages[0]
features = [f.strip() for f in args.features.split(",")]
print(f"Creating website: {args.name}")
print(f"Type: {args.type}")
print(f"Languages: {languages}")
print(f"Features: {features}")
print(f"Output: {args.output}")
# Interactive analytics setup (if not in no-interactive mode)
analytics_config = None
if not args.no_interactive:
analytics_config = ask_analytics_setup()
# Create project structure
create_project(args, languages, default_locale, features)
# Save analytics configuration to project
if analytics_config:
save_analytics_config(args.output, analytics_config)
# Auto-setup Umami if credentials provided
umami_website_id = args.umami_id
if args.auto_setup_umami and (
not analytics_config or analytics_config.get("analytics_type") == "umami"
):
print("\n📈 Setting up Umami Analytics...")
try:
from umami_integration import setup_umami_for_website
website_domain = args.name.lower().replace(" ", "-") + ".moreminimore.com"
success, result = setup_umami_for_website(
args.umami_url,
args.umami_username,
args.umami_password,
args.name,
website_domain,
args.output,
)
if success:
umami_website_id = result["website_id"]
print(f" ✓ Umami website created: {umami_website_id}")
else:
print(
f" ⚠ Umami setup skipped: {result.get('error', 'Unknown error')}"
)
except Exception as e:
print(f" ⚠ Umami setup failed: {e}")
print(" Continuing without Umami...")
print(f"\n✅ Website created successfully at: {args.output}")
# Update .env with Umami ID if auto-setup
env_file = os.path.join(args.output, ".env")
if os.path.exists(env_file) and umami_website_id:
with open(env_file, "a", encoding="utf-8") as f:
f.write(f"\n# Umami Analytics (auto-configured)\n")
f.write(f"UMAMI_WEBSITE_ID={umami_website_id}\n")
print(f" ✓ Umami ID added to .env")
print("\nNext steps:")
print(f" 1. cd {args.output}")
print(" 2. npm install")
print(" 3. Update .env with your credentials")
print(" 4. npm run dev")
# Always ask to sync (skip if no-interactive mode)
print("")
print("=" * 60)
print("🏠 Website created locally!")
print("=" * 60)
print("")
print("Preview at: http://localhost:4321")
print("")
# Ask if they want to sync to Gitea/Easypanel
if args.no_interactive:
print("✅ Done! Website is ready at:", args.output)
print("To sync later, run the sync command manually.")
return
sync_choice = (
input("Do you want to sync to Gitea and deploy to Easypanel? (y/n): ")
.strip()
.lower()
)
if sync_choice != "y":
print("")
print("✅ Done! Website is ready at:", args.output)
print(
"To sync later, run this script again or use gitea-sync/easypanel-deploy skills."
)
return
print("")
print("Proceeding with sync and deployment...")
print("")
# Step 1: Sync to Gitea
print("📦 Step 1/3: Syncing to Gitea...")
git_url = sync_to_gitea(args.output, args.name)
# Step 2: Deploy to Easypanel
print("")
print("🚀 Step 2/3: Deploying to Easypanel...")
deployment_url = deploy_to_easypanel(args.output, args.name, git_url)
# Step 3: Verify and monitor
print("")
print("📊 Step 3/3: Monitoring deployment...")
monitor_deployment(args.name)
# Final output
print("")
print("=" * 60)
print("✅ COMPLETE!")
print("=" * 60)
print("")
print(f"📁 Website generated: {args.output}")
print(f"🌐 Gitea Repository: {git_url.replace('.git', '')}")
print(f"🚀 Easypanel Deployment: {deployment_url}")
print("")
print("📋 Next steps:")
print(f" 1. Website is deploying to: {deployment_url}")
print(f" 2. Check status at: https://panelwebsite.moreminimore.com")
print(f" 3. Edit Umami config: cd {args.output} && nano .env")
print("")
def save_analytics_config(output_path: str, config: dict):
"""Save analytics configuration to project context"""
context_dir = os.path.join(output_path, "context")
os.makedirs(context_dir, exist_ok=True)
# Save data-services.json
data_services = {
"ga4": {
"enabled": config.get("analytics_type") == "ga4",
"property_id": config.get("ga4_property_id", ""),
"credentials_path": config.get("ga4_credentials_path", ""),
}
if config.get("analytics_type") == "ga4"
else {"enabled": False},
"gsc": {
"enabled": config.get("search_console") is not None,
"site_url": "",
"credentials_path": config.get("search_console", {}).get(
"credentials_path", ""
),
},
"umami": {
"enabled": config.get("analytics_type") == "umami",
"api_url": os.getenv("UMAMI_URL", ""),
"website_id": config.get("umami_website_id", ""),
}
if config.get("analytics_type") == "umami"
else {"enabled": False},
"dataforseo": {"enabled": False},
}
with open(
os.path.join(context_dir, "data-services.json"), "w", encoding="utf-8"
) as f:
json.dump(data_services, f, indent=2)
print(f" ✓ Analytics config saved to context/data-services.json")
# ============================================================================
# PROJECT CREATION FUNCTIONS
# ============================================================================
def create_project(args, languages, default_locale, features):
"""Create the Astro project structure with templates."""
output_path = Path(args.output)
project_name = args.name.lower().replace(" ", "-")
site_url = f"https://{project_name}.moreminimore.com"
# Get template directory
script_dir = Path(__file__).parent
template_dir = script_dir / "templates"
print("\n📁 Creating project structure...")
# Create directories
dirs = [
output_path / "public" / "images",
output_path / "public" / "images" / "icons",
output_path / "src" / "components" / "common",
output_path / "src" / "components" / "consent",
output_path / "src" / "components" / "ui",
output_path / "src" / "layouts",
output_path / "src" / "pages",
output_path / "src" / "pages" / default_locale,
output_path / "src" / "styles",
output_path / "src" / "content" / "blog",
output_path / "src" / "lib",
output_path / "db",
]
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
print(" ✓ Directory structure created")
# Copy templates if they exist
if template_dir.exists():
print(" 📦 Copying templates with IDs...")
# Copy layouts
layout_src = template_dir / "layouts" / "BaseLayout.astro"
if layout_src.exists():
content = layout_src.read_text(encoding="utf-8")
content = content.replace(
"const siteName = 'Website Name'", f"const siteName = '{args.name}'"
)
content = content.replace(
"const siteUrl = 'https://example.com'", f"const siteUrl = '{site_url}'"
)
(output_path / "src" / "layouts" / "BaseLayout.astro").write_text(
content, encoding="utf-8"
)
# Copy Header
header_src = template_dir / "components" / "common" / "Header.astro"
if header_src.exists():
shutil.copy(
header_src,
output_path / "src" / "components" / "common" / "Header.astro",
)
# Copy Footer
footer_src = template_dir / "components" / "common" / "Footer.astro"
if footer_src.exists():
shutil.copy(
footer_src,
output_path / "src" / "components" / "common" / "Footer.astro",
)
# Copy page templates
page_src = template_dir / "pages" / "index.astro"
if page_src.exists():
shutil.copy(
page_src, output_path / "src" / "pages" / default_locale / "index.astro"
)
# Copy styles
style_src = template_dir / "styles" / "global.css"
if style_src.exists():
shutil.copy(style_src, output_path / "src" / "styles" / "global.css")
# Copy LINE icon
line_icon_src = template_dir / "icons" / "line.svg"
if line_icon_src.exists():
icons_dir = output_path / "public" / "images" / "icons"
icons_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(line_icon_src, icons_dir / "line.svg")
print(" ✓ LINE icon copied")
print(" ✓ Templates copied")
# Create astro.config.mjs
locales_str = ", ".join([f"'{lang}'" for lang in languages])
astro_config = ASTRO_CONFIG_TEMPLATE.format(
site_url=site_url, locales=locales_str, default_locale=default_locale
)
(output_path / "astro.config.mjs").write_text(astro_config, encoding="utf-8")
print(" ✓ astro.config.mjs created")
# Create package.json
package_json = PACKAGE_JSON_TEMPLATE.format(name=project_name)
(output_path / "package.json").write_text(package_json, encoding="utf-8")
print(" ✓ package.json created")
# Create tsconfig.json
tsconfig = """{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
"""
(output_path / "tsconfig.json").write_text(tsconfig, encoding="utf-8")
# Create env file
env_content = f"""# Website Configuration
SITE_NAME={args.name}
SITE_URL={site_url}
# Umami Analytics (optional - get from Umami dashboard)
# UMAMI_WEBSITE_ID=
# UMAMI_URL=
"""
(output_path / ".env").write_text(env_content, encoding="utf-8")
print(" ✓ Configuration files created")
# Create basic index page if no template
if not (output_path / "src" / "pages" / default_locale / "index.astro").exists():
index_content = f"""---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/common/Header.astro';
import Footer from '../components/common/Footer.astro';
---
<BaseLayout title="Home" description="Welcome to {args.name}">
<Header />
<main id="main-content">
<section id="hero-section" class="hero">
<h1 id="hero-title">Welcome to {args.name}</h1>
<p id="hero-subtitle">Your trusted partner</p>
</section>
</main>
<Footer />
</BaseLayout>
"""
(output_path / "src" / "pages" / default_locale / "index.astro").write_text(
index_content, encoding="utf-8"
)
print(" ✓ Basic pages created")
# Create Dockerfile
dockerfile = f"""FROM node:20-slim
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Copy source
COPY . .
# Build
RUN npm run build
# Serve
EXPOSE 80
CMD ["npm", "run", "preview"]
"""
(output_path / "Dockerfile").write_text(dockerfile, encoding="utf-8")
print(" ✓ Dockerfile created")
# Create .gitignore
gitignore = """# Dependencies
node_modules/
# Build output
dist/
# Environment
.env
.env.*
!.env.example
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
"""
(output_path / ".gitignore").write_text(gitignore, encoding="utf-8")
print(" ✓ .gitignore created")
return output_path
def sync_to_gitea(output_path: str, repo_name: str) -> str:
"""Sync project to Gitea repository."""
try:
# Import gitea sync functionality
sys.path.insert(0, str(Path(__file__).parent.parent / "gitea-sync" / "scripts"))
from sync import sync_repo
# Use the gitea-sync script
result = sync_repo(
repo_name=repo_name,
repo_path=output_path,
description=f"Website: {repo_name}",
auto_push=True,
)
if result.get("success"):
return result.get(
"url", f"https://git.moreminimore.com/user/{repo_name}.git"
)
else:
print(f" ⚠ Gitea sync failed: {result.get('error')}")
return f"https://git.moreminimore.com/user/{repo_name}.git"
except Exception as e:
print(f" ⚠ Gitea sync error: {e}")
print(" Continuing without Gitea sync...")
# Return a dummy URL so deployment can continue
return f"https://git.moreminimore.com/user/{repo_name}.git"
def deploy_to_easypanel(output_path: str, project_name: str, git_url: str) -> str:
"""Deploy project to Easypanel."""
try:
# Import easypanel deploy functionality
sys.path.insert(
0, str(Path(__file__).parent.parent / "easypanel-deploy" / "scripts")
)
from deploy import (
get_session_token,
create_service,
update_git_source,
update_build_type,
deploy_service,
load_env,
)
# Load credentials
env = load_env()
username = env.get("EASYPANEL_USERNAME", "")
password = env.get("EASYPANEL_PASSWORD", "")
if not username or not password:
print(" ⚠ Easypanel credentials not found")
print(" Skipping deployment - you can deploy manually later")
return f"https://{project_name}.moreminimore.com"
# Get session token
token = get_session_token(username, password)
if not token:
print(" ⚠ Failed to get Easypanel session")
return f"https://{project_name}.moreminimore.com"
# Create service
create_service(project_name, "web", token)
# Update git source
update_git_source(project_name, "web", git_url, "main", token)
# Set build type to dockerfile
update_build_type(project_name, "web", token, "dockerfile")
# Deploy
deploy_service(project_name, "web", token)
return f"https://{project_name}.moreminimore.com"
except Exception as e:
print(f" ⚠ Easypanel deployment error: {e}")
print(" Continuing without deployment...")
return f"https://{project_name}.moreminimore.com"
def monitor_deployment(project_name: str):
"""Monitor deployment status."""
print(f" 📊 Monitoring deployment for {project_name}...")
print(" (Deployment is running in background)")
print(" Check status at: https://panelwebsite.moreminimore.com")
if __name__ == "__main__":
main()

View File

@@ -1,561 +0,0 @@
#!/usr/bin/env python3
"""
Smart Website Migration - Detect, Plan, then Migrate
This script intelligently migrates existing websites by:
1. Detecting current tech stack and versions
2. Creating a detailed migration plan
3. Preserving ALL inline CSS and content exactly
4. Converting CSS frameworks (Tailwind v3 → v4, etc.)
5. Reinstalling Astro fresh
6. Adding new features without breaking existing functionality
Workflow:
1. ANALYZE - Detect tech stack, versions, CSS framework
2. PLAN - Create detailed migration plan
3. BACKUP - Create full backup
4. PRESERVE - Extract inline CSS and content from each page
5. CONVERT - Convert CSS to match target tech stack
6. REBUILD - Fresh Astro install with preserved content
7. ENHANCE - Add new features (cookie consent, PDPA, etc.)
8. TEST - Verify build and all pages
Usage:
python3 migrate_existing_website.py \
--input "./existing-website" \
--output "./migrated-website" \
--plan-only # Just create plan, don't migrate
"""
import os
import sys
import json
import shutil
import re
import subprocess
import argparse
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
import sys
import json
import shutil
import re
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
class TechStackDetector:
"""Detect tech stack and versions from existing website."""
def __init__(self, website_path: Path):
self.website_path = website_path
self.detected = {}
def detect_all(self) -> Dict[str, Any]:
"""Run all detection methods."""
print("🔍 Detecting tech stack...\n")
self.detect_astro_version()
self.detect_node_version()
self.detect_css_framework()
self.detect_tailwind_version()
self.detect_pages_structure()
self.detect_content_collections()
self.detect_integrations()
self.detect_custom_css()
return self.detected
def detect_astro_version(self):
"""Detect Astro version from package.json."""
package_json = self.website_path / 'package.json'
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
deps = package_data.get('dependencies', {})
dev_deps = package_data.get('devDependencies', {})
astro_version = deps.get('astro') or dev_deps.get('astro')
self.detected['astro'] = {
'version': astro_version or 'unknown',
'detected': True
}
print(f" ✓ Astro version: {astro_version}")
else:
print(f" ✗ package.json not found")
self.detected['astro'] = {'version': 'unknown', 'detected': False}
def detect_node_version(self):
"""Detect required Node.js version."""
package_json = self.website_path / 'package.json'
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
engines = package_data.get('engines', {})
node_version = engines.get('node', '>=18.0.0')
self.detected['node'] = {
'required_version': node_version,
'detected': True
}
print(f" ✓ Node.js: {node_version}")
def detect_css_framework(self):
"""Detect CSS framework (Tailwind, Bootstrap, etc.)."""
package_json = self.website_path / 'package.json'
css_frameworks = {
'tailwindcss': 'Tailwind CSS',
'bootstrap': 'Bootstrap',
'bulma': 'Bulma',
'foundation': 'Foundation',
'semantic-ui': 'Semantic UI',
'material-ui': 'Material UI',
'@chakra-ui/core': 'Chakra UI',
}
detected_frameworks = []
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
deps = {**package_data.get('dependencies', {}), **package_data.get('devDependencies', {})}
for pkg, name in css_frameworks.items():
if pkg in deps:
detected_frameworks.append({
'name': name,
'package': pkg,
'version': deps[pkg]
})
self.detected['css_framework'] = {
'frameworks': detected_frameworks,
'primary': detected_frameworks[0]['name'] if detected_frameworks else 'Custom CSS',
'detected': len(detected_frameworks) > 0
}
if detected_frameworks:
print(f" ✓ CSS Framework: {detected_frameworks[0]['name']}")
else:
print(f" ✓ CSS: Custom/Inline")
def detect_tailwind_version(self):
"""Detect Tailwind CSS version."""
package_json = self.website_path / 'package.json'
tailwind_config = self.website_path / 'tailwind.config.js'
tailwind_config_ts = self.website_path / 'tailwind.config.ts'
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
deps = {**package_data.get('dependencies', {}), **package_data.get('devDependencies', {})}
if 'tailwindcss' in deps:
version = deps['tailwindcss']
major_version = version.replace('^', '').replace('~', '').split('.')[0]
# Check for v4 features
has_v4_features = False
if tailwind_config.exists():
with open(tailwind_config) as f:
config = f.read()
# v4 uses different config format
has_v4_features = '@theme' in config or 'import theme' in config
self.detected['tailwind'] = {
'version': version,
'major_version': int(major_version) if major_version.isdigit() else 3,
'config_file': 'tailwind.config.js' if tailwind_config.exists() else 'tailwind.config.ts' if tailwind_config_ts.exists() else None,
'needs_upgrade': int(major_version) < 4 if major_version.isdigit() else False,
'detected': True
}
print(f" ✓ Tailwind CSS v{major_version}: {'Needs upgrade to v4' if int(major_version) < 4 else 'Up to date'}")
def detect_pages_structure(self):
"""Detect pages structure."""
pages_dir = self.website_path / 'src' / 'pages'
if pages_dir.exists():
pages = list(pages_dir.glob('**/*.astro'))
pages.extend(list(pages_dir.glob('**/*.md')))
pages.extend(list(pages_dir.glob('**/*.mdx')))
self.detected['pages'] = {
'count': len(pages),
'structure': 'flat' if len(list(pages_dir.glob('*.astro'))) > len(pages) / 2 else 'nested',
'has_i18n': any('/th/' in str(p) or '(th)' in str(p) for p in pages),
'detected': True
}
print(f" ✓ Pages: {len(pages)} pages detected")
def detect_content_collections(self):
"""Detect Astro Content Collections."""
content_dir = self.website_path / 'src' / 'content'
content_config = self.website_path / 'src' / 'content.config.ts'
collections = []
if content_dir.exists():
for subdir in content_dir.iterdir():
if subdir.is_dir() and not subdir.name.startswith('_'):
collection_files = list(subdir.glob('*.md')) + list(subdir.glob('*.mdx'))
if collection_files:
collections.append({
'name': subdir.name,
'file_count': len(collection_files)
})
self.detected['content_collections'] = {
'collections': collections,
'has_config': content_config.exists(),
'detected': len(collections) > 0
}
if collections:
print(f" ✓ Content Collections: {len(collections)} collections")
def detect_integrations(self):
"""Detect Astro integrations."""
astro_config = self.website_path / 'astro.config.mjs'
astro_config_ts = self.website_path / 'astro.config.ts'
config_file = astro_config if astro_config.exists() else astro_config_ts if astro_config_ts.exists() else None
integrations = []
if config_file:
with open(config_file) as f:
config_content = f.read()
# Detect common integrations
integration_patterns = {
'tailwind': 'tailwind()',
'react': 'react()',
'vue': 'vue()',
'svelte': 'svelte()',
'solid': 'solid()',
'mdx': 'mdx()',
'sitemap': 'sitemap()',
'vercel': 'vercel()',
'netlify': 'netlify()',
'node': 'node()',
'static-adapter': 'staticAdapter',
}
for name, pattern in integration_patterns.items():
if pattern in config_content:
integrations.append(name)
self.detected['integrations'] = {
'integrations': integrations,
'config_file': config_file.name if config_file else None,
'detected': len(integrations) > 0
}
if integrations:
print(f" ✓ Integrations: {', '.join(integrations)}")
def detect_custom_css(self):
"""Detect custom CSS files and inline styles."""
src_dir = self.website_path / 'src'
css_files = []
inline_styles = 0
if src_dir.exists():
# Find CSS files
for css_file in src_dir.glob('**/*.css'):
css_files.append(str(css_file.relative_to(self.website_path)))
# Count inline styles in Astro files
for astro_file in src_dir.glob('**/*.astro'):
with open(astro_file) as f:
content = f.read()
# Count style tags
inline_styles += content.count('<style>')
self.detected['custom_css'] = {
'css_files': css_files,
'inline_style_count': inline_styles,
'detected': len(css_files) > 0 or inline_styles > 0
}
print(f" ✓ Custom CSS: {len(css_files)} files, {inline_styles} inline styles")
class MigrationPlanner:
"""Create detailed migration plan."""
def __init__(self, tech_stack: Dict[str, Any], input_path: Path, output_path: Path):
self.tech_stack = tech_stack
self.input_path = input_path
self.output_path = output_path
self.plan = {}
def create_plan(self) -> Dict[str, Any]:
"""Create comprehensive migration plan."""
print("\n📋 Creating migration plan...\n")
self.plan['summary'] = self._create_summary()
self.plan['preservation'] = self._plan_preservation()
self.plan['css_conversion'] = self._plan_css_conversion()
self.plan['rebuild'] = self._plan_rebuild()
self.plan['enhancements'] = self._plan_enhancements()
self.plan['testing'] = self._plan_testing()
self.plan['risks'] = self._identify_risks()
return self.plan
def _create_summary(self) -> Dict[str, Any]:
"""Create migration summary."""
astro_version = self.tech_stack.get('astro', {}).get('version', 'unknown')
css_framework = self.tech_stack.get('css_framework', {}).get('primary', 'Unknown')
tailwind_version = self.tech_stack.get('tailwind', {}).get('major_version', 0)
page_count = self.tech_stack.get('pages', {}).get('count', 0)
return {
'source_astro_version': astro_version,
'target_astro_version': 'latest (5.x)',
'css_framework': css_framework,
'tailwind_upgrade': f"v{tailwind_version} → v4" if tailwind_version < 4 else "No upgrade needed",
'page_count': page_count,
'estimated_time': f"{max(10, page_count * 2)} minutes"
}
def _plan_preservation(self) -> Dict[str, Any]:
"""Plan content preservation."""
return {
'steps': [
'Extract all inline CSS from .astro files',
'Extract all page content (frontmatter + body)',
'Copy all static assets (public/ folder)',
'Copy all images and media files',
'Copy all content collections (blog, products, etc.)',
'Preserve all component logic and scripts',
'Keep all existing routes and URLs'
],
'preserved_exactly': [
'All page content (text, images, links)',
'All inline styles (<style> tags)',
'All component functionality',
'All existing URLs and routes',
'All metadata (title, description, etc.)'
]
}
def _plan_css_conversion(self) -> Dict[str, Any]:
"""Plan CSS framework conversion."""
tailwind = self.tech_stack.get('tailwind', {})
needs_upgrade = tailwind.get('needs_upgrade', False)
steps = []
if needs_upgrade:
steps.extend([
'Backup existing tailwind.config.js',
'Install Tailwind CSS v4',
'Convert tailwind.config.js to v4 format',
'Update CSS imports to v4 syntax',
'Test all pages for CSS issues',
'Fix any breaking changes'
])
else:
steps.append('No CSS framework upgrade needed')
return {
'needs_conversion': needs_upgrade,
'steps': steps,
'breaking_changes': [
'Tailwind v4 uses different config format',
'Some utilities may have changed',
'Custom CSS may need adjustment'
] if needs_upgrade else []
}
def _plan_rebuild(self) -> Dict[str, Any]:
"""Plan Astro rebuild."""
return {
'steps': [
'Create fresh Astro 5.x project',
'Install all required integrations',
'Migrate preserved content to new structure',
'Apply CSS conversions',
'Update Astro config for new features',
'Add new components (cookie consent, etc.)'
],
'fresh_install': True,
'keep_existing_components': True
}
def _plan_enhancements(self) -> Dict[str, Any]:
"""Plan new features to add."""
return {
'new_features': [
'PDPA-compliant Privacy Policy (Thai law)',
'PDPA-compliant Terms of Service (Thai law)',
'Working cookie consent (blocks cookies until consent)',
'Consent logging database',
'Umami Analytics integration',
'i18n routing (Thai/English)',
'Admin dashboard for consent logs'
],
'optional_features': [
'Blog post templates',
'Product pages',
'Contact forms',
'SEO optimization'
]
}
def _plan_testing(self) -> Dict[str, Any]:
"""Plan testing steps."""
return {
'pre_deploy_tests': [
'Docker build completes successfully',
'All pages load without errors',
'All inline CSS renders correctly',
'Cookie consent blocks cookies until accepted',
'All links work',
'Mobile responsive design works',
'Backend functions work (forms, databases)',
'Analytics tracking works (if consented)'
],
'manual_verification': [
'Compare migrated pages with originals',
'Verify all content is preserved',
'Test cookie consent functionality',
'Test on multiple browsers',
'Test on mobile devices'
]
}
def _identify_risks(self) -> List[Dict[str, str]]:
"""Identify potential risks."""
risks = []
if self.tech_stack.get('astro', {}).get('version', 'unknown') == 'unknown':
risks.append({
'risk': 'Astro version unknown',
'impact': 'Migration may require manual adjustments',
'mitigation': 'Manual review of package.json required'
})
inline_styles = self.tech_stack.get('custom_css', {}).get('inline_style_count', 0)
if inline_styles > 50:
risks.append({
'risk': f'High inline CSS count ({inline_styles} styles)',
'impact': 'May take longer to verify all styles',
'mitigation': 'Automated CSS extraction and verification'
})
tailwind = self.tech_stack.get('tailwind', {})
if tailwind.get('needs_upgrade', False):
risks.append({
'risk': 'Tailwind v3 → v4 upgrade',
'impact': 'Some CSS utilities may break',
'mitigation': 'Thorough CSS testing on all pages'
})
return risks
def main():
parser = argparse.ArgumentParser(
description='Smart Website Migration - Detect, Plan, then Migrate'
)
parser.add_argument('--input', '-i', required=True, help='Input directory (existing website)')
parser.add_argument('--output', '-o', required=True, help='Output directory (migrated website)')
parser.add_argument('--plan-only', action='store_true', help='Only create plan, don\'t migrate')
parser.add_argument('--languages', default='th,en', help='Languages (comma-separated)')
args = parser.parse_args()
input_path = Path(args.input)
output_path = Path(args.output)
if not input_path.exists():
print(f"❌ Error: Input directory '{input_path}' does not exist")
sys.exit(1)
print("=" * 70)
print("🔄 SMART WEBSITE MIGRATION")
print("=" * 70)
print(f"\n📁 Input: {input_path}")
print(f"📁 Output: {output_path}")
print(f"📋 Plan only: {args.plan_only}")
print()
# Step 1: Detect tech stack
detector = TechStackDetector(input_path)
tech_stack = detector.detect_all()
# Step 2: Create migration plan
planner = MigrationPlanner(tech_stack, input_path, output_path)
plan = planner.create_plan()
# Save plan to file
plan_file = output_path.parent / f"migration_plan_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
output_path.mkdir(parents=True, exist_ok=True)
with open(plan_file, 'w') as f:
json.dump({
'tech_stack': tech_stack,
'migration_plan': plan,
'created_at': datetime.now().isoformat()
}, f, indent=2)
print(f"\n📄 Migration plan saved to: {plan_file}")
# Print plan summary
print("\n" + "=" * 70)
print("📋 MIGRATION PLAN SUMMARY")
print("=" * 70)
summary = plan.get('summary', {})
print(f"\n📊 Summary:")
print(f" • Astro: {summary.get('source_astro_version', 'unknown')}{summary.get('target_astro_version', 'latest')}")
print(f" • CSS: {summary.get('css_framework', 'Unknown')}")
print(f" • Tailwind: {summary.get('tailwind_upgrade', 'N/A')}")
print(f" • Pages: {summary.get('page_count', 0)} pages")
print(f" • Estimated time: {summary.get('estimated_time', 'unknown')}")
# Print risks
risks = plan.get('risks', [])
if risks:
print(f"\n⚠️ Risks identified: {len(risks)}")
for risk in risks:
print(f"{risk['risk']}")
print(f" Impact: {risk['impact']}")
print(f" Mitigation: {risk['mitigation']}")
if args.plan_only:
print("\n✅ Plan created successfully!")
print("\nTo proceed with migration, run:")
print(f" python3 migrate_existing_website.py \\")
print(f" --input '{input_path}' \\")
print(f" --output '{output_path}'")
else:
print("\n⚠️ WARNING: Full migration not yet implemented!")
print("\nThis is a safety measure. The migration script will:")
print(" 1. Review this plan carefully")
print(" 2. Manually verify all detected tech stack")
print(" 3. Approve the migration plan")
print(" 4. Then we'll implement the full migration logic")
print("\nPlease review the plan and let us know if you want to proceed!")
print("\n" + "=" * 70)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
requests>=2.28.0

View File

@@ -1,313 +0,0 @@
---
// Password-protected admin page for viewing consent logs
import { db, ConsentLog, desc } from 'astro:db';
// Simple password protection (in production, use proper auth)
const ADMIN_PASSWORD = Astro.env.ADMIN_PASSWORD || 'changeme';
let logs = [];
let isAuthenticated = false;
let error = '';
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const password = formData.get('password');
if (password === ADMIN_PASSWORD) {
isAuthenticated = true;
try {
logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
} catch (err) {
error = 'Failed to load consent logs. Make sure database is initialized.';
console.error(err);
}
} else {
error = 'Invalid password';
}
}
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Consent Logs Admin | PDPA Compliance</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f3f4f6;
padding: 2rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1.5rem;
color: #111827;
}
.login-form {
max-width: 400px;
background: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: #374151;
}
input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
}
input[type="password"]:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
button {
width: 100%;
padding: 0.75rem 1.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #1d4ed8;
}
.error {
background: #fee2e2;
color: #dc2626;
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.success {
background: #dcfce7;
color: #16a34a;
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
table {
width: 100%;
background: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
th {
background: #f9fafb;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
}
tr:hover {
background: #f9fafb;
}
.actions {
margin-bottom: 1rem;
}
.btn {
display: inline-block;
padding: 0.5rem 1rem;
font-size: 0.875rem;
border-radius: 0.375rem;
text-decoration: none;
transition: background 0.2s;
}
.btn-primary {
background: #2563eb;
color: white;
}
.btn-primary:hover {
background: #1d4ed8;
}
.btn-danger {
background: #dc2626;
color: white;
border: none;
cursor: pointer;
}
.btn-danger:hover {
background: #b91c1c;
}
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 9999px;
font-weight: 500;
}
.badge-green {
background: #dcfce7;
color: #16a34a;
}
.badge-red {
background: #fee2e2;
color: #dc2626;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Consent Logs Admin Dashboard</h1>
{!isAuthenticated ? (
<div class="login-form">
<h2 class="text-xl font-bold mb-4">Admin Login</h2>
{error && <div class="error">{error}</div>}
<form method="POST">
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
required
placeholder="Enter admin password"
/>
</div>
<button type="submit">Login</button>
</form>
<p class="mt-4 text-sm text-gray-600">
Default password: <code>changeme</code> (change in .env)
</p>
</div>
) : (
<div>
<div class="actions flex gap-4 mb-4">
<a href="/admin/consent-logs" class="btn btn-primary">Refresh</a>
<a href="/" class="btn" style="background: #6b7280; color: white;">← Back to Site</a>
</div>
{error && <div class="error">{error}</div>}
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Date/Time</th>
<th>Locale</th>
<th>Session ID</th>
<th>Essential</th>
<th>Analytics</th>
<th>Marketing</th>
<th>Policy Ver</th>
<th>IP Hash</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{logs.length === 0 ? (
<tr>
<td colspan="9" style="text-align: center; padding: 2rem;">
No consent logs found. Make sure the website has received consent.
</td>
</tr>
) : (
logs.map((log) => (
<tr>
<td>{new Date(log.timestamp).toLocaleString('en-GB')}</td>
<td>{log.locale.toUpperCase()}</td>
<td style="font-family: monospace; font-size: 0.75rem;">{log.sessionId}</td>
<td>
<span class="badge badge-green">{log.essential ? 'Yes' : 'No'}</span>
</td>
<td>
{log.analytics ? (
<span class="badge badge-green">✓</span>
) : (
<span class="badge badge-red">✗</span>
)}
</td>
<td>
{log.marketing ? (
<span class="badge badge-green">✓</span>
) : (
<span class="badge badge-red">✗</span>
)}
</td>
<td>{log.policyVersion}</td>
<td style="font-family: monospace; font-size: 0.75rem;">{log.ipHash}</td>
<td>
<button
class="btn btn-danger"
onclick="deleteConsent('{log.sessionId}')"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
>
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div style="margin-top: 1rem; padding: 1rem; background: #fef3c7; border-radius: 0.375rem;">
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.5rem;">⚠️ Important Notes:</h3>
<ul style="font-size: 0.75rem; color: #92400e; list-style: disc; padding-left: 1.5rem;">
<li>Consent records must be retained for 10 years (PDPA requirement)</li>
<li>Only delete records when user exercises "right to be forgotten"</li>
<li>Document all deletions for compliance audit</li>
<li>IP addresses are hashed for privacy protection</li>
</ul>
</div>
</div>
)}
</div>
<script>
async function deleteConsent(sessionId) {
if (!confirm('Delete this consent record? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/consent/${sessionId}`, {
method: 'DELETE',
});
if (response.ok) {
alert('Consent record deleted successfully');
location.reload();
} else {
alert('Failed to delete consent record');
}
} catch (error) {
console.error('Delete error:', error);
alert('Error deleting consent record');
}
}
</script>
</body>
</html>

View File

@@ -1,135 +0,0 @@
---
const currentYear = new Date().getFullYear();
const quickLinks = [
{ name: 'หน้าแรก', href: '/' },
{ name: 'เกี่ยวกับเรา', href: '/about' },
{ name: 'บริการ', href: '/services' },
{ name: 'สินค้า', href: '/products' },
{ name: 'ติดต่อเรา', href: '/contact' },
];
const services = [
{ name: 'บริการติดตั้ง', href: '/services/installation' },
{ name: 'บริการให้คำปรึกษา', href: '/services/consultation' },
{ name: 'บริการซ่อมบำรุง', href: '/services/maintenance' },
];
const legalLinks = [
{ name: 'นโยบายความเป็นส่วนตัว', href: '/privacy-policy' },
{ name: 'ข้อกำหนดและเงื่อนไข', href: '/terms-and-conditions' },
{ name: 'นโยบายคุกกี้', href: '/cookie-policy' },
];
const socialLinks = [
{ name: 'Facebook', href: 'https://facebook.com', icon: 'facebook', svg: '' },
{ name: 'Line', href: 'https://line.me', icon: 'line', svg: 'line' },
{ name: 'YouTube', href: 'https://youtube.com', icon: 'youtube', svg: '' },
];
---
<footer id="footer-component" class="bg-secondary-900 text-white pt-16 pb-8">
<div id="footer-container" class="container-custom">
<!-- Main Footer Content -->
<div id="footer-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
<!-- Company Info -->
<div id="footer-company">
<div id="footer-logo-container" class="mb-4">
<img id="footer-logo" src="/images/logo.png" alt="Logo" class="h-12" />
</div>
<div id="footer-description">
<p class="text-secondary-300 mb-4">
บริษัท ดีล พลัส เทค จำกัด ผู้เชี่ยวชาญด้านระบบท่อและอุปกรณ์ติดตั้งคุณภาพสูง
</p>
</div>
<!-- Social Links -->
<div id="footer-social">
<div id="social-links-container" class="flex space-x-4">
{socialLinks.map((social) => (
<a id={`social-${social.icon}`} href={social.href} target="_blank" rel="noopener noreferrer" class="w-10 h-10 bg-secondary-800 rounded-full flex items-center justify-center hover:bg-primary-600 transition-colors" aria-label={social.name}>
{social.svg === 'line' ? (
<img src="/images/icons/line.svg" alt="LINE" class="w-5 h-5" />
) : (
<span class="text-sm font-medium">{social.name[0]}</span>
)}
</a>
))}
</div>
</div>
</div>
<!-- Quick Links -->
<div id="footer-quick-links">
<h3 id="quick-links-title" class="text-lg font-bold mb-4">ลิงก์ด่วน</h3>
<ul id="quick-links-list" class="space-y-2">
{quickLinks.map((link, index) => (
<li>
<a id={`quick-link-${index}`} href={link.href} class="text-secondary-300 hover:text-white transition-colors">
{link.name}
</a>
</li>
))}
</ul>
</div>
<!-- Services -->
<div id="footer-services">
<h3 id="services-title" class="text-lg font-bold mb-4">บริการ</h3>
<ul id="services-list" class="space-y-2">
{services.map((service, index) => (
<li>
<a id={`service-link-${index}`} href={service.href} class="text-secondary-300 hover:text-white transition-colors">
{service.name}
</a>
</li>
))}
</ul>
</div>
<!-- Contact Info -->
<div id="footer-contact">
<h3 id="contact-title" class="text-lg font-bold mb-4">ติดต่อเรา</h3>
<div id="contact-info" class="space-y-3">
<div id="contact-address" class="flex items-start">
<span class="text-secondary-400 mr-2">📍</span>
<span class="text-secondary-300">123 ถนนสุขุมวิท กรุงเทพมหานคร 10110</span>
</div>
<div id="contact-phone" class="flex items-center">
<span class="text-secondary-400 mr-2">📞</span>
<a href="tel:021234567" class="text-secondary-300 hover:text-white transition-colors">02-123-4567</a>
</div>
<div id="contact-email" class="flex items-center">
<span class="text-secondary-400 mr-2">✉️</span>
<a href="mailto:info@example.com" class="text-secondary-300 hover:text-white transition-colors">info@example.com</a>
</div>
<div id="contact-hours" class="flex items-center">
<span class="text-secondary-400 mr-2">🕐</span>
<span class="text-secondary-300">วันจันทร์-เสาร์ 08:00-18:00 น.</span>
</div>
</div>
</div>
</div>
<!-- Bottom Footer -->
<div id="footer-bottom" class="border-t border-secondary-800 pt-8">
<div id="footer-bottom-content" class="flex flex-col md:flex-row justify-between items-center gap-4">
<div id="copyright">
<p class="text-secondary-400 text-sm">
© {currentYear} บริษัท ดีล พลัส เทค จำกัด สงวนลิขสิทธิ์
</p>
</div>
<div id="footer-legal-links">
<ul id="legal-links-list" class="flex flex-wrap gap-4 text-sm">
{legalLinks.map((link, index) => (
<li>
<a id={`legal-link-${index}`} href={link.href} class="text-secondary-400 hover:text-white transition-colors">
{link.name}
</a>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</footer>

View File

@@ -1,122 +0,0 @@
---
const navItems = [
{ name: 'หน้าแรก', href: '/' },
{ name: 'เกี่ยวกับเรา', href: '/about' },
{ name: 'บริการ', href: '/services' },
{ name: 'ติดต่อเรา', href: '/contact' },
];
const categories = [
{ name: 'สินค้า', href: '/products', hasDropdown: true },
];
---
<header id="header-component" class="fixed w-full top-0 z-40 bg-white shadow-md">
<nav id="navbar" class="container-custom">
<div id="navbar-container" class="flex items-center justify-between h-16 md:h-20">
<!-- Logo -->
<div id="logo-container">
<a id="logo-link" href="/" class="flex items-center">
<img id="logo-image" src="/images/logo.png" alt="Logo" class="h-10 md:h-12" />
</a>
</div>
<!-- Desktop Navigation -->
<div id="desktop-nav" class="hidden md:flex items-center space-x-6">
<div id="nav-items-container">
{navItems.map((item) => (
<a id={`nav-${item.name.replace(' ', '-').toLowerCase()}`} href={item.href} class="nav-link text-secondary-700 hover:text-primary-600 font-medium transition-colors">
{item.name}
</a>
))}
</div>
<!-- Categories Dropdown -->
<div id="categories-dropdown" class="relative group">
<button id="categories-btn" class="nav-link flex items-center text-secondary-700 hover:text-primary-600 font-medium transition-colors">
สินค้า
<svg id="categories-chevron" class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div id="categories-menu" class="absolute left-0 mt-2 w-48 bg-white rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-left">
<div id="categories-menu-container" class="py-2">
<a id="category-all-products" href="/products" class="block px-4 py-2 text-secondary-700 hover:bg-primary-50 hover:text-primary-600">
สินค้าทั้งหมด
</a>
<a id="category-pipes" href="/products/pipes" class="block px-4 py-2 text-secondary-700 hover:bg-primary-50 hover:text-primary-600">
ท่อ
</a>
<a id="category-valves" href="/products/valves" class="block px-4 py-2 text-secondary-700 hover:bg-primary-50 hover:text-primary-600">
วาล์ว
</a>
<a id="category-fittings" href="/products/fittings" class="block px-4 py-2 text-secondary-700 hover:bg-primary-50 hover:text-primary-600">
ข้อต่อ
</a>
</div>
</div>
</div>
<!-- CTA Button -->
<div id="cta-container">
<a id="cta-button" href="/contact" class="btn-primary px-4 py-2 rounded-lg font-medium">
ติดต่อเรา
</a>
</div>
</div>
<!-- Mobile Menu Button -->
<div id="mobile-menu-btn-container" class="md:hidden">
<button id="mobile-menu-btn" class="p-2 text-secondary-700 hover:text-primary-600" aria-label="เมนู">
<svg id="menu-icon" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden md:hidden bg-white border-t">
<div id="mobile-menu-container" class="px-4 py-4 space-y-2">
{navItems.map((item, index) => (
<a id={`mobile-nav-${index}`} href={item.href} class="block py-2 text-secondary-700 hover:text-primary-600 font-medium">
{item.name}
</a>
))}
<div id="mobile-categories-container">
<button id="mobile-categories-btn" class="flex items-center justify-between w-full py-2 text-secondary-700 font-medium">
สินค้า
<svg id="mobile-chevron" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div id="mobile-categories-menu" class="hidden pl-4 space-y-2">
<a id="mobile-all-products" href="/products" class="block py-2 text-secondary-600">- สินค้าทั้งหมด</a>
<a id="mobile-pipes" href="/products/pipes" class="block py-2 text-secondary-600">- ท่อ</a>
<a id="mobile-valves" href="/products/valves" class="block py-2 text-secondary-600">- วาล์ว</a>
<a id="mobile-fittings" href="/products/fittings" class="block py-2 text-secondary-600">- ข้อต่อ</a>
</div>
</div>
<a id="mobile-cta" href="/contact" class="block w-full text-center btn-primary px-4 py-3 rounded-lg font-medium mt-4">
ติดต่อเรา
</a>
</div>
</div>
</nav>
</header>
<script>
// Mobile Menu Toggle
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
const mobileCategoriesBtn = document.getElementById('mobile-categories-btn');
const mobileCategoriesMenu = document.getElementById('mobile-categories-menu');
mobileMenuBtn?.addEventListener('click', () => {
mobileMenu?.classList.toggle('hidden');
});
mobileCategoriesBtn?.addEventListener('click', () => {
mobileCategoriesMenu?.classList.toggle('hidden');
});
</script>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-6,-6)">
<path d="M12.5,42L35.5,42C39.09,42 42,39.09 42,35.5L42,12.5C42,8.91 39.09,6 35.5,6L12.5,6C8.91,6 6,8.91 6,12.5L6,35.5C6,39.09 8.91,42 12.5,42Z" style="fill:rgb(0,195,0);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-6,-6)">
<path d="M37.113,22.417C37.113,16.552 31.233,11.78 24.006,11.78C16.779,11.78 10.898,16.552 10.898,22.417C10.898,27.675 15.561,32.079 21.86,32.912C22.287,33.004 22.868,33.194 23.015,33.558C23.147,33.889 23.101,34.408 23.057,34.743C23.057,34.743 22.904,35.668 22.87,35.865C22.813,36.196 22.607,37.161 24.005,36.572C25.404,35.983 31.553,32.127 34.303,28.961L34.302,28.961C36.203,26.879 37.113,24.764 37.113,22.417ZM18.875,25.907L16.271,25.907C15.892,25.907 15.584,25.599 15.584,25.219L15.584,20.01C15.584,19.631 15.892,19.323 16.271,19.323C16.65,19.323 16.958,19.631 16.958,20.01L16.958,24.531L18.875,24.531C19.254,24.531 19.562,24.839 19.562,25.218C19.562,25.598 19.254,25.907 18.875,25.907ZM21.568,25.219C21.568,25.598 21.26,25.907 20.881,25.907C20.502,25.907 20.194,25.599 20.194,25.219L20.194,20.01C20.194,19.631 20.502,19.323 20.881,19.323C21.26,19.323 21.568,19.631 21.568,20.01L21.568,25.219ZM27.838,25.219C27.838,25.516 27.65,25.778 27.368,25.871C27.297,25.895 27.223,25.907 27.15,25.907C26.935,25.907 26.73,25.804 26.601,25.632L23.932,21.997L23.932,25.219C23.932,25.598 23.624,25.907 23.244,25.907C22.865,25.907 22.556,25.599 22.556,25.219L22.556,20.01C22.556,19.714 22.745,19.452 23.026,19.358C23.097,19.334 23.17,19.323 23.244,19.323C23.458,19.323 23.664,19.426 23.793,19.598L26.463,23.233L26.463,20.01C26.463,19.631 26.772,19.323 27.151,19.323C27.53,19.323 27.838,19.631 27.838,20.01L27.838,25.219ZM32.052,21.927C32.431,21.927 32.74,22.235 32.74,22.615C32.74,22.994 32.432,23.302 32.052,23.302L30.135,23.302L30.135,24.532L32.052,24.532C32.431,24.532 32.74,24.84 32.74,25.219C32.74,25.598 32.431,25.907 32.052,25.907L29.448,25.907C29.07,25.907 28.761,25.599 28.761,25.219L28.761,20.011C28.761,19.632 29.069,19.324 29.448,19.324L32.052,19.324C32.431,19.324 32.74,19.632 32.74,20.011C32.74,20.39 32.432,20.698 32.052,20.698L30.135,20.698L30.135,21.928L32.052,21.928L32.052,21.927Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,190 +0,0 @@
---
import '../styles/global.css';
interface Props {
title: string;
description?: string;
image?: string;
canonicalURL?: string;
}
const { title, description = '', image = '/images/logo.png', canonicalURL = Astro.url } = Astro.props;
const siteName = 'Website Name';
const siteUrl = 'https://example.com';
---
<!doctype html>
<html lang="th">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content={Astro.generator} />
<!-- SEO Meta Tags -->
<title>{title} | {siteName}</title>
<meta name="title" content={`${title} | ${siteName}`} />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={`${title} | ${siteName}`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, siteUrl)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={`${title} | ${siteName}`} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, siteUrl)} />
<!-- Favicon -->
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
</head>
<body id="body" class="flex flex-col min-h-screen">
<div id="page-wrapper">
<header id="header">
<slot name="header" />
</header>
<main id="main-content" class="flex-grow">
<slot />
</main>
<footer id="footer">
<slot name="footer" />
</footer>
<!-- Cookie Consent Banner -->
<div id="cookie-consent-banner" class="fixed bottom-0 left-0 right-0 z-50 bg-secondary-900 text-white p-6 shadow-2xl transform translate-y-full transition-transform duration-500">
<div id="cookie-consent-container" class="container-custom max-w-6xl">
<div id="cookie-consent-content" class="flex flex-col md:flex-row items-center justify-between gap-6">
<div id="cookie-consent-text" class="flex-1">
<h3 id="cookie-consent-title" class="text-xl font-bold mb-2">เราใช้คุกกี้เพื่อประสบการณ์ที่ดีที่สุด</h3>
<p id="cookie-consent-description" class="text-secondary-300 text-base">
เว็บไซต์ของเราใช้คุกกี้เพื่อเพิ่มประสิทธิภาพการใช้งาน คุณสามารถยอมรับหรือปฏิเสธได้
</p>
</div>
<div id="cookie-consent-buttons" class="flex flex-wrap gap-4">
<button id="cookie-reject-btn" class="btn-secondary px-6 py-3 text-sm">
ปฏิเสธทั้งหมด
</button>
<button id="cookie-accept-btn" class="btn-primary px-6 py-3 text-sm">
ยอมรับทั้งหมด
</button>
<button id="cookie-settings-btn" class="btn-outline px-6 py-3 text-sm">
ตั้งค่า
</button>
</div>
</div>
</div>
</div>
<!-- Cookie Preferences Modal -->
<div id="cookie-preferences-modal" class="fixed inset-0 z-50 hidden bg-black/50">
<div id="cookie-modal-content" class="flex items-center justify-center min-h-screen p-4">
<div id="cookie-modal-box" class="bg-white rounded-2xl shadow-2xl max-w-lg w-full p-6">
<div id="cookie-modal-header" class="flex justify-between items-center mb-4">
<h2 id="cookie-modal-title" class="text-xl font-bold">ตั้งค่าคุกกี้</h2>
<button id="cookie-modal-close" class="text-gray-500 hover:text-gray-700">
<span id="cookie-close-icon">✕</span>
</button>
</div>
<div id="cookie-modal-body">
<div id="cookie-necessary" class="mb-4">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium">คุกกี้ที่จำเป็น</h3>
<p class="text-sm text-gray-600">จำเป็นสำหรับการทำงานของเว็บไซต์</p>
</div>
<input type="checkbox" checked disabled class="w-5 h-5" />
</div>
</div>
<div id="cookie-analytics" class="mb-4">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium">คุกกี้วิเคราะห์</h3>
<p class="text-sm text-gray-600">ช่วยให้เราเข้าใจผู้ใช้งาน</p>
</div>
<input type="checkbox" id="cookie-analytics-checkbox" class="w-5 h-5" />
</div>
</div>
<div id="cookie-marketing" class="mb-4">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium">คุกกี้การตลาด</h3>
<p class="text-sm text-gray-600">ใช้สำหรับโฆษณา</p>
</div>
<input type="checkbox" id="cookie-marketing-checkbox" class="w-5 h-5" />
</div>
</div>
</div>
<div id="cookie-modal-footer" class="flex justify-end gap-4 mt-6">
<button id="cookie-save-btn" class="btn-primary px-6 py-3">บันทึก</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Cookie Consent Logic
const banner = document.getElementById('cookie-consent-banner');
const acceptBtn = document.getElementById('cookie-accept-btn');
const rejectBtn = document.getElementById('cookie-reject-btn');
const settingsBtn = document.getElementById('cookie-settings-btn');
const modal = document.getElementById('cookie-preferences-modal');
const closeModal = document.getElementById('cookie-modal-close');
const saveBtn = document.getElementById('cookie-save-btn');
function showBanner() {
banner?.classList.remove('translate-y-full');
}
function hideBanner() {
banner?.classList.add('translate-y-full');
}
acceptBtn?.addEventListener('click', () => {
localStorage.setItem('cookie-consent', 'accepted');
hideBanner();
});
rejectBtn?.addEventListener('click', () => {
localStorage.setItem('cookie-consent', 'rejected');
hideBanner();
});
settingsBtn?.addEventListener('click', () => {
modal?.classList.remove('hidden');
});
closeModal?.addEventListener('click', () => {
modal?.classList.add('hidden');
});
saveBtn?.addEventListener('click', () => {
const analytics = (document.getElementById('cookie-analytics-checkbox') as HTMLInputElement)?.checked;
const marketing = (document.getElementById('cookie-marketing-checkbox') as HTMLInputElement)?.checked;
localStorage.setItem('cookie-consent', JSON.stringify({ analytics, marketing }));
modal?.classList.add('hidden');
hideBanner();
});
// Check consent on load
const consent = localStorage.getItem('cookie-consent');
if (!consent) {
showBanner();
}
</script>
</body>
</html>

View File

@@ -1,183 +0,0 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/common/Header.astro';
import Footer from '../components/common/Footer.astro';
const pageTitle = 'หน้าแรก';
const pageDescription = 'ผู้เชี่ยวชาญด้านระบบท่อและอุปกรณ์ติดตั้งคุณภาพสูง ราคาโรงงาน';
---
<BaseLayout title={pageTitle} description={pageDescription}>
<Header slot="header" />
<!-- Hero Section -->
<section id="hero-section" class="relative bg-white section overflow-hidden pt-24 md:pt-32">
<div id="hero-container" class="container-custom">
<div id="hero-grid" class="grid md:grid-cols-2 gap-8 md:gap-12 items-center">
<div id="hero-content" class="animate-fade-in">
<h1 id="hero-title" class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-bold text-secondary-900 mb-4 md:mb-6 leading-tight">
ผู้เชี่ยวชาญระบบน้ำ<br/>
<span class="text-green-600">คุณภาพสูง ราคาโรงงาน</span>
</h1>
<p id="hero-description" class="text-base sm:text-lg md:text-xl text-secondary-600 mb-6 md:mb-8 leading-relaxed">
เราเป็นผู้เชี่ยวชาญด้านระบบน้ำ ให้คำแนะนำและจำหน่ายท่อ PPR ท่อ HDPE ท่อ PVC และอุปกรณ์ติดตั้งคุณภาพสูง ราคาถูก
</p>
<div id="hero-buttons" class="flex flex-wrap justify-center gap-3 md:gap-4">
<a id="hero-cta-products" href="/products" class="bg-green-600 hover:bg-green-700 text-white px-5 py-3 md:px-8 md:py-4 rounded-xl font-medium transition-all hover:shadow-lg active:scale-95 text-sm md:text-lg">
ดูสินค้าทั้งหมด
</a>
<a id="hero-cta-contact" href="/contact" class="bg-white text-green-600 px-5 py-3 md:px-8 md:py-4 rounded-xl border-2 border-green-500 font-medium transition-all hover:shadow-lg active:scale-95 text-sm md:text-lg">
ติดต่อเรา
</a>
</div>
<div id="hero-stats" class="flex items-center sm:space-x-8 space-x-4 sm:mt-12 mt-8 justify-center">
<div id="stat-experience">
<div id="stat-experience-value" class="text-xl sm:text-2xl md:text-3xl font-bold text-green-600">10+</div>
<div id="stat-experience-label" class="text-secondary-600 text-xs sm:text-base">ปีประสบการณ์</div>
</div>
<div id="stat-projects">
<div id="stat-projects-value" class="text-xl sm:text-2xl md:text-3xl font-bold text-green-600">1000+</div>
<div id="stat-projects-label" class="text-secondary-600 text-xs sm:text-base">โปรเจคต์</div>
</div>
<div id="stat-products">
<div id="stat-products-value" class="text-xl sm:text-2xl md:text-3xl font-bold text-green-600">500+</div>
<div id="stat-products-label" class="text-secondary-600 text-xs sm:text-base">สินค้า</div>
</div>
</div>
</div>
<div id="hero-image-container" class="relative animate-slide-up mt-6 md:mt-0">
<div id="hero-image-wrapper" class="absolute inset-0 bg-gradient-to-br from-green-500/20 to-accent-500/20 rounded-3xl blur-3xl"></div>
<div id="hero-image-grid" class="grid grid-cols-3 gap-2 md:gap-4 relative">
<div id="hero-image-main" class="col-span-2 row-span-2">
<img id="hero-img-1" src="/images/hero-1.jpg" alt="Products" class="w-full h-full object-cover rounded-2xl shadow-xl" />
</div>
<div id="hero-image-2">
<img id="hero-img-2" src="/images/hero-2.jpg" alt="Products" class="w-full h-full object-cover rounded-2xl shadow-xl" />
</div>
<div id="hero-image-3">
<img id="hero-img-3" src="/images/hero-3.jpg" alt="Products" class="w-full h-full object-cover rounded-2xl shadow-xl" />
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Categories Section -->
<section id="categories-section" class="py-16 md:py-24 bg-gray-50">
<div id="categories-container" class="container-custom">
<div id="categories-header" class="text-center mb-12">
<h2 id="categories-title" class="text-2xl md:text-3xl lg:text-4xl font-bold text-secondary-900 mb-4">
หมวดสินค้า
</h2>
<p id="categories-subtitle" class="text-secondary-600 text-lg max-w-2xl mx-auto">
สินค้าคุณภาพสูงสำหรับทุกการใช้งาน
</p>
</div>
<div id="categories-grid" class="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
{['ท่อ PPR', 'ท่อ HDPE', 'ท่อ UPVC', 'วาล์ว', 'ข้อต่อ', 'อุปกรณ์ติดตั้ง', 'ปั๊มน้ำ', 'อุปกรณ์ดับเพลิง'].map((category, index) => (
<a id={`category-card-${index}`} href={`/products/${category.toLowerCase().replace(' ', '-')}`} class="group bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 p-6 text-center">
<div id={`category-icon-${index}`} class="w-16 h-16 mx-auto mb-4 bg-primary-100 rounded-full flex items-center justify-center group-hover:bg-primary-600 transition-colors">
<span class="text-2xl">{category[0]}</span>
</div>
<h3 id={`category-name-${index}`} class="font-bold text-secondary-900 group-hover:text-primary-600 transition-colors">
{category}
</h3>
</a>
))}
</div>
</div>
</section>
<!-- Featured Products Section -->
<section id="featured-products-section" class="py-16 md:py-24 bg-white">
<div id="featured-products-container" class="container-custom">
<div id="featured-products-header" class="text-center mb-12">
<h2 id="featured-products-title" class="text-2xl md:text-3xl lg:text-4xl font-bold text-secondary-900 mb-4">
สินค้าแนะนำ
</h2>
<p id="featured-products-subtitle" class="text-secondary-600 text-lg max-w-2xl mx-auto">
สินค้ายอดนิยมจากลูกค้า
</p>
</div>
<div id="featured-products-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{[
{ name: 'ท่อ PPR ตราช้าง', description: 'ท่อ PPR คุณภาพสูง มาตรฐาน', image: '/images/products/ppr.jpg' },
{ name: 'วาล์วน้ำดับเพลิง', description: 'วาล์วคุณภาพสูง ทนทาน', image: '/images/products/valve.jpg' },
{ name: 'ข้อต่อ HDPE', description: 'ข้อต่อสำหรับท่อ HDPE', image: '/images/products/fitting.jpg' },
].map((product, index) => (
<div id={`featured-product-card-${index}`} class="bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden">
<div id={`featured-product-image-${index}`} class="aspect-video overflow-hidden">
<img id={`featured-product-img-${index}`} src={product.image} alt={product.name} class="w-full h-full object-cover hover:scale-105 transition-transform duration-300" />
</div>
<div id={`featured-product-content-${index}`} class="p-6">
<h3 id={`featured-product-title-${index}`} class="font-bold text-lg text-secondary-900 mb-2">
{product.name}
</h3>
<p id={`featured-product-desc-${index}`} class="text-secondary-600 mb-4">
{product.description}
</p>
<a id={`featured-product-link-${index}`} href={`/products/${product.name.toLowerCase().replace(' ', '-')}`} class="text-primary-600 font-medium hover:text-primary-700 transition-colors">
ดูรายละเอียด →
</a>
</div>
</div>
))}
</div>
<div id="featured-products-cta" class="text-center mt-12">
<a id="featured-products-all" href="/products" class="btn-primary px-8 py-3 text-lg rounded-xl">
ดูสินค้าทั้งหมด
</a>
</div>
</div>
</section>
<!-- Why Choose Us Section -->
<section id="why-choose-us-section" class="py-16 md:py-24 bg-primary-50">
<div id="why-choose-us-container" class="container-custom">
<div id="why-choose-us-header" class="text-center mb-12">
<h2 id="why-choose-us-title" class="text-2xl md:text-3xl lg:text-4xl font-bold text-secondary-900 mb-4">
ทำไมต้องเลือกเรา
</h2>
<p id="why-choose-us-subtitle" class="text-secondary-600 text-lg max-w-2xl mx-auto">
เรามีความมุ่งมั่นในการให้บริการที่ดีที่สุด
</p>
</div>
<div id="why-choose-us-grid" class="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
{[
{ icon: '🏭', title: 'โรงงานผู้ผลิต', description: 'สินค้าจากโรงงานโดยตรง ราคาถูก' },
{ icon: '✅', title: 'มาตรฐาน', description: 'ผ่านการรับรอง มอก.' },
{ icon: '🚚', title: 'จัดส่งรวดเร็ว', description: 'ส่งทั่วประเทศไทย' },
].map((feature, index) => (
<div id={`why-choose-us-card-${index}`} class="bg-white rounded-xl shadow-md p-8 text-center">
<div id={`why-choose-us-icon-${index}`} class="text-4xl mb-4">{feature.icon}</div>
<h3 id={`why-choose-us-feature-title-${index}`} class="font-bold text-xl text-secondary-900 mb-2">{feature.title}</h3>
<p id={`why-choose-us-feature-desc-${index}`} class="text-secondary-600">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
<!-- CTA Section -->
<section id="cta-section" class="py-16 md:py-24 bg-green-600">
<div id="cta-container" class="container-custom text-center">
<h2 id="cta-title" class="text-2xl md:text-3xl lg:text-4xl font-bold text-white mb-4">
ต้องการคำปรึกษาฟรี?
</h2>
<p id="cta-description" class="text-white/80 text-lg mb-8 max-w-2xl mx-auto">
ทีมงานของเราพร้อมให้คำปรึกษาฟรี ไม่มีค่าใช้จ่าย
</p>
<div id="cta-buttons" class="flex flex-wrap justify-center gap-4">
<a id="cta-contact-btn" href="/contact" class="bg-white text-green-600 px-8 py-3 rounded-xl font-bold hover:bg-gray-100 transition-colors">
ติดต่อเราวันนี้
</a>
<a id="cta-line-btn" href="https://line.me" target="_blank" class="bg-green-500 text-white px-8 py-3 rounded-xl font-bold hover:bg-green-400 transition-colors">
ติดต่อผ่าน LINE
</a>
</div>
</div>
</section>
<Footer slot="footer" />
</BaseLayout>

View File

@@ -1,298 +0,0 @@
/* Global Styles */
/* Base Typography */
html {
font-size: 18px;
}
@media (min-width: 1280px) {
html { font-size: 20px; }
}
@media (min-width: 1536px) {
html { font-size: 22px; }
}
@media (min-width: 1920px) {
html { font-size: 24px; }
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
}
/* Minimum font sizes */
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
/* Container */
.container-custom {
max-width: 1280px;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 640px) {
.container-custom {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
/* Section */
.section {
padding-top: 3rem;
padding-bottom: 3rem;
}
@media (min-width: 768px) {
.section {
padding-top: 4rem;
padding-bottom: 4rem;
}
}
/* Buttons */
.btn-primary {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #16a34a;
color: white;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: #15803d;
}
.btn-secondary {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #374151;
color: white;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-outline {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: transparent;
color: white;
border: 2px solid white;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-outline:hover {
background-color: white;
color: #16a34a;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out;
}
.animate-slide-up {
animation: slideUp 0.5s ease-out;
}
/* Utility Classes */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.gap-8 { gap: 2rem; }
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (min-width: 640px) {
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 768px) {
.md\:flex { display: flex; }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Spacing */
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.p-2 { padding: 0.5rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.p-8 { padding: 2rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
/* Colors */
.text-white { color: white; }
.text-black { color: black; }
.text-gray-500 { color: #6b7280; }
.text-gray-600 { color: #4b5563; }
.text-gray-700 { color: #374151; }
.text-gray-900 { color: #111827; }
.text-green-500 { color: #22c55e; }
.text-green-600 { color: #16a34a; }
.bg-white { background-color: white; }
.bg-gray-50 { background-color: #f9fafb; }
.bg-gray-100 { background-color: #f3f4f6; }
.bg-black { background-color: black; }
.bg-green-500 { background-color: #22c55e; }
.bg-green-600 { background-color: #16a34a; }
/* Border Radius */
.rounded { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-2xl { border-radius: 1rem; }
.rounded-full { border-radius: 9999px; }
/* Shadows */
.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }
.shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); }
/* Typography */
.font-bold { font-weight: 700; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.text-3xl { font-size: 1.875rem; }
.text-4xl { font-size: 2.25rem; }
.leading-tight { line-height: 1.25; }
.leading-relaxed { line-height: 1.625; }
/* Width/Height */
.w-full { width: 100%; }
.h-full { height: 100%; }
.h-10 { height: 2.5rem; }
.h-12 { height: 3rem; }
.h-16 { height: 4rem; }
.min-h-screen { min-height: 100vh; }
/* Position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-0 { top: 0; }
.right-0 { right: 0; }
.bottom-0 { bottom: 0; }
.left-0 { left: 0; }
.z-40 { z-index: 40; }
.z-50 { z-index: 50; }
/* Overflow */
.overflow-hidden { overflow: hidden; }
/* Transitions */
.transition-all { transition: all 0.2s; }
.transition-colors { transition: color 0.2s, background-color 0.2s; }
.transition-transform { transition: transform 0.2s; }
/* Transform */
.translate-y-full { transform: translateY(100%); }
/* Misc */
.cursor-pointer { cursor: pointer; }
.hover\:scale-105:hover { transform: scale(1.05); }
.active\:scale-95:active { transform: scale(0.95); }
/* Hidden by default */
.hidden { display: none; }
/* Space-x for flex items */
.space-x-4 > * + * { margin-left: 1rem; }
.space-x-6 > * + * { margin-left: 1.5rem; }
.space-x-8 > * + * { margin-left: 2rem; }
/* Space-y for flex/grid items */
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-4 > * + * { margin-top: 1rem; }
.space-y-6 > * + * { margin-top: 1.5rem; }
/* Aspect ratio */
.aspect-video {
aspect-ratio: 16 / 9;
}
/* Object fit */
.object-cover {
object-fit: cover;
}
/* Border */
.border {
border-width: 1px;
border-style: solid;
}
.border-t {
border-top-width: 1px;
border-style: solid;
}
/* Text align */
.text-center { text-align: center; }
/* Max width */
.max-w-lg { max-width: 32rem; }
.max-w-2xl { max-width: 42rem; }
.max-w-6xl { max-width: 72rem; }
.mx-auto { margin-left: auto; margin-right: auto; }

View File

@@ -1,423 +0,0 @@
# นโยบายความเป็นส่วนตัว (Privacy Policy)
**ชื่อเว็บไซต์:** [WEBSITE_NAME]
**มีผลบังคับใช้วันที่:** [DATE]
**แก้ไขล่าสุด:** [DATE]
## 1. บทนำ
บริษัท [COMPANY_NAME] ("เรา", "ของเรา" หรือ "บริษัท") ให้คำมั่นสัญญาที่จะปกป้องข้อมูลส่วนบุคคลของผู้ใช้บริการ ("ผู้ใช้", "ของคุณ" หรือ "ท่าน") ที่ใช้งานเว็บไซต์ [WEBSITE_URL] ("เว็บไซต์") นโยบายความเป็นส่วนตัวฉบับนี้อธิบายถึงวิธีการเก็บรวบรวม ใช้ เปิดเผย และคุ้มครองข้อมูลส่วนบุคคลของท่าน
นโยบายนี้จัดทำขึ้นตามกฎหมายคุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 (PDPA) และกฎหมายที่เกี่ยวข้องของประเทศไทย
## 2. ข้อมูลส่วนบุคคลที่เก็บรวบรวม
### 2.1 ข้อมูลที่ท่านให้โดยตรง
เราอาจเก็บรวบรวมข้อมูลส่วนบุคคลต่อไปนี้ที่ท่านให้โดยตรง:
**ข้อมูลการติดต่อ:**
- ชื่อและนามสกุล
- ที่อยู่อีเมล
- เบอร์โทรศัพท์
- ที่อยู่สำหรับติดต่อ
**ข้อมูลบัญชีผู้ใช้:**
- ชื่อผู้ใช้ (Username)
- รหัสผ่าน (Password)
- ประวัติการใช้งาน
**ข้อมูลการชำระเงิน:**
- ข้อมูลบัตรเครดิต/เดบิต
- ข้อมูลบัญชีธนาคาร
- ประวัติการทำธุรกรรม
**ข้อมูลอื่นๆ:**
- ความคิดเห็น ข้อเสนอแนะ
- แบบสำรวจความพึงพอใจ
- เนื้อหาที่ท่านส่งมา
### 2.2 ข้อมูลที่เก็บรวบรวมโดยอัตโนมัติ
เมื่อท่านใช้งานเว็บไซต์ เราอาจเก็บรวบรวมข้อมูลต่อไปนี้โดยอัตโนมัติ:
**ข้อมูลอุปกรณ์:**
- ประเภทของอุปกรณ์ (คอมพิวเตอร์, สมาร์ทโฟน, แท็บเล็ต)
- ระบบปฏิบัติการ
- เบราว์เซอร์ที่ใช้
- ที่อยู่ IP (IP Address)
**ข้อมูลการใช้งาน:**
- หน้าเว็บที่ท่านเข้าชม
- เวลาและวันที่เข้าชม
- ระยะเวลาการใช้งาน
- ลิงก์ที่ท่านคลิก
- ข้อมูล Cookie
**ข้อมูลตำแหน่ง:**
- ข้อมูลตำแหน่งทางภูมิศาสตร์ (หากท่านอนุญาต)
## 3. วัตถุประสงค์ในการใช้ข้อมูล
เราใช้ข้อมูลส่วนบุคคลของท่านเพื่อวัตถุประสงค์ดังต่อไปนี้:
### 3.1 การให้บริการ
- ให้บริการและบำรุงรักษาเว็บไซต์
- ประมวลผลคำขอและธุรกรรมของท่าน
- ส่งมอบสินค้าหรือบริการที่ท่านสั่งซื้อ
- จัดการบัญชีผู้ใช้ของท่าน
### 3.2 การสื่อสาร
- ตอบกลับคำถามและข้อร้องเรียน
- ส่งข้อมูลเกี่ยวกับบริการของเรา
- แจ้งเตือนเกี่ยวกับการอัปเดตหรือการเปลี่ยนแปลง
- ส่งข่าวสารโปรโมชั่น (หากท่านยินยอม)
### 3.3 การปรับปรุงบริการ
- วิเคราะห์การใช้งานเว็บไซต์
- พัฒนาและปรับปรุงบริการ
- ทดสอบฟีเจอร์ใหม่
- วิจัยตลาด
### 3.4 ความปลอดภัย
- ระบุและป้องกันภัยคุกคามด้านความปลอดภัย
- ตรวจสอบกิจกรรมที่อาจเป็นการฉ้อโกง
- บังคับใช้นโยบายและข้อกำหนดของเรา
- ปฏิบัติตามข้อกำหนดทางกฎหมาย
### 3.5 ตามกฎหมาย
- ปฏิบัติตามภาระผูกพันทางกฎหมาย
- ตอบสนองต่อคำขอจากหน่วยงานราชการ
- ป้องกันสิทธิและทรัพย์สินของเรา
- ป้องกันอันตรายต่อสาธารณะ
## 4. ฐานทางกฎหมายในการประมวลผลข้อมูล
เราประมวลผลข้อมูลส่วนบุคคลของท่านบนฐานทางกฎหมายดังต่อไปนี้:
### 4.1 ความยินยอม (Consent)
ท่านได้ให้ความยินยอมให้เราประมวลผลข้อมูลส่วนบุคคลของท่านเพื่อวัตถุประสงค์เฉพาะ เช่น:
- การส่งข่าวสารทางอีเมล
- การใช้ Cookie สำหรับการตลาด
- การเก็บข้อมูลสุขภาพหรือข้อมูลอ่อนไหวอื่นๆ
### 4.2 การปฏิบัติตามสัญญา (Contract)
การประมวลผลจำเป็นสำหรับการปฏิบัติตามสัญญาที่ท่านทำกับเรา เช่น:
- การประมวลผลการสั่งซื้อ
- การให้บริการที่ท่านร้องขอ
- การจัดการบัญชีผู้ใช้
### 4.3 หน้าที่ทางกฎหมาย (Legal Obligation)
การประมวลผลจำเป็นเพื่อปฏิบัติตามภาระผูกพันทางกฎหมาย เช่น:
- การเก็บรักษาบันทึกทางการเงิน
- การรายงานต่อหน่วยงานราชการ
- การปฏิบัติตามคำสั่งศาล
### 4.4 ผลประโยชน์โดยชอบด้วยกฎหมาย (Legitimate Interest)
การประมวลผลจำเป็นเพื่อประโยชน์โดยชอบด้วยกฎหมายของเรา เช่น:
- การป้องกันและการตรวจสอบการฉ้อโกง
- ความปลอดภัยของเครือข่ายและข้อมูล
- การปรับปรุงบริการ
## 5. การเปิดเผยข้อมูลให้กับบุคคลที่สาม
เราไม่ขายหรือให้เช่าข้อมูลส่วนบุคคลของท่านให้กับบุคคลที่สาม อย่างไรก็ตาม เราอาจเปิดเผยข้อมูลของท่านในกรณีต่อไปนี้:
### 5.1 ผู้ให้บริการ (Service Providers)
เราอาจแบ่งปันข้อมูลกับผู้ให้บริการที่ช่วยเราดำเนินธุรกิจ:
- **ผู้ให้บริการชำระเงิน:** เช่น ธนาคาร, ผู้ให้บริการบัตรเครดิต
- **ผู้ให้บริการจัดส่ง:** เช่น ไปรษณีย์ไทย, Kerry, Flash Express
- **ผู้ให้บริการคลาวด์:** เช่น AWS, Google Cloud, Azure
- **ผู้ให้บริการอีเมล:** เช่น SendGrid, Mailchimp
- **ผู้ให้บริการวิเคราะห์ข้อมูล:** เช่น Google Analytics
### 5.2 หน่วยงานราชการ
เราอาจเปิดเผยข้อมูลเมื่อได้รับคำสั่งตามกฎหมาย:
- ศาลหรือกระบวนการยุติธรรม
- หน่วยงานบังคับใช้กฎหมาย
- หน่วยงานกำกับดูแล
- หน่วยงานภาษี
### 5.3 การโอนกิจการ
ในกรณีที่มีการควบรวมกิจการ ขายทรัพย์สิน หรือการโอนกิจการ ข้อมูลของท่านอาจถูกโอนไปยังผู้ซื้อหรือผู้รับโอน
### 5.4 เพื่อปกป้องสิทธิ
เราอาจเปิดเผยข้อมูลเพื่อ:
- ปกป้องสิทธิ ทรัพย์สิน หรือความปลอดภัยของเรา
- ป้องกันการฉ้อโกง
- ปฏิบัติตามข้อกำหนดการใช้งาน
## 6. การเก็บรักษาข้อมูล
เราเก็บรักษาข้อมูลส่วนบุคคลของท่านไว้เฉพาะเท่าที่จำเป็นเพื่อวัตถุประสงค์ที่ระบุไว้ในนโยบายนี้:
### 6.1 ระยะเวลาการเก็บรักษา
- **ข้อมูลบัญชีผู้ใช้:** เก็บรักษาตราบเท่าที่ท่านเป็นผู้ใช้บริการ และ 3 ปีหลังจากนั้น
- **ข้อมูลธุรกรรม:** 5 ปี ตามข้อกำหนดของกฎหมายภาษี
- **ข้อมูลการติดต่อ:** 2 ปีหลังจากการติดต่อล่าสุด
- **ข้อมูล Cookie:** ตามการตั้งค่า Cookie ของท่าน
### 6.2 การทำลายข้อมูล
เมื่อไม่จำเป็นต้องเก็บรักษาข้อมูลต่อไป เราจะ:
- ลบข้อมูลจากระบบอิเล็กทรอนิกส์
- ทำลายเอกสารที่เป็นกระดาษ
- ทำให้ข้อมูลไม่สามารถระบุตัวตนได้
## 7. สิทธิของท่าน
ภายใต้ PDPA ท่านมีสิทธิดังต่อไปนี้เกี่ยวกับข้อมูลส่วนบุคคลของท่าน:
### 7.1 สิทธิในการเข้าถึง (Right to Access)
ท่านมีสิทธิขอเข้าถึงข้อมูลส่วนบุคคลที่ท่านเป็นเจ้าของ:
- ขอสำเนาข้อมูลส่วนบุคคล
- ทราบวัตถุประสงค์ของการประมวลผล
- ทราบแหล่งที่มาของข้อมูล
### 7.2 สิทธิในการแก้ไข (Right to Rectification)
ท่านมีสิทธิขอให้แก้ไขข้อมูลส่วนบุคคลที่ไม่ถูกต้อง:
- แก้ไขข้อมูลการติดต่อ
- อัปเดตข้อมูลบัญชี
- แก้ไขข้อมูลอื่นๆ
### 7.3 สิทธิในการลบ (Right to Erasure)
ท่านมีสิทธิขอให้ลบข้อมูลส่วนบุคคลในกรณีต่อไปนี้:
- ข้อมูลไม่จำเป็นต้องใช้แล้ว
- ท่านถอนความยินยอม
- ข้อมูลถูกประมวลผลโดยมิชอบด้วยกฎหมาย
### 7.4 สิทธิในการจำกัดการประมวลผล (Right to Restriction)
ท่านมีสิทธิขอให้จำกัดการประมวลผลข้อมูล:
- ขณะตรวจสอบความถูกต้องของข้อมูล
- เมื่อการประมวลผลเป็นการมิชอบด้วยกฎหมาย
- เมื่อเราไม่จำเป็นต้องใช้ข้อมูลแล้ว แต่ท่านต้องการให้เก็บไว้เพื่อการใช้สิทธิทางกฎหมาย
### 7.5 สิทธิในการคัดค้าน (Right to Object)
ท่านมีสิทธิคัดค้านการประมวลผลข้อมูล:
- การประมวลผลเพื่อประโยชน์โดยชอบด้วยกฎหมาย
- การประมวลผลเพื่อการตลาดโดยตรง
- การประมวลผลเพื่อวัตถุประสงค์ทางสถิติ
### 7.6 สิทธิในการโอนย้ายข้อมูล (Right to Data Portability)
ท่านมีสิทธิขอให้โอนข้อมูลส่วนบุคคลไปยังผู้ควบคุมข้อมูลอื่น:
- ข้อมูลที่ท่านให้ไว้
- ข้อมูลที่ประมวลผลโดยอัตโนมัติ
- เมื่อการประมวลผลอาศัยความยินยอมหรือสัญญา
### 7.7 สิทธิในการถอนความยินยอม (Right to Withdraw Consent)
หากการประมวลผลอาศัยความยินยอม ท่านมีสิทธิถอนความยินยอมเมื่อใดก็ได้:
- การถอนความยินยอมไม่กระทบต่อการประมวลผลก่อนหน้า
- ท่านอาจไม่สามารถใช้บริการบางอย่างได้หลังถอนความยินยอม
### 7.8 สิทธิในการร้องเรียน (Right to Complaint)
หากท่านเชื่อว่าข้อมูลของท่านถูกประมวลผลโดยมิชอบด้วยกฎหมาย ท่านมีสิทธิร้องเรียนต่อ:
- สำนักงานคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล (สคส.)
- เว็บไซต์: www.pdpc.or.th
- โทรศัพท์: 0-2141-6900
## 8. Cookie และเทคโนโลยีการติดตาม
### 8.1 Cookie คืออะไร
Cookie เป็นไฟล์ข้อความขนาดเล็กที่เว็บไซต์บันทึกลงในอุปกรณ์ของท่านเมื่อท่านเข้าชมเว็บไซต์
### 8.2 ประเภทของ Cookie ที่เราใช้
**Cookie ที่จำเป็น (Necessary Cookies):**
- จำเป็นสำหรับการทำงานของเว็บไซต์
- ไม่สามารถปิดใช้งานได้
- ไม่เก็บข้อมูลส่วนบุคคล
**Cookie เพื่อประสิทธิภาพ (Performance Cookies):**
- รวบรวมข้อมูลเกี่ยวกับวิธีการใช้เว็บไซต์
- ช่วยให้เราปรับปรุงเว็บไซต์
- ข้อมูลเป็นแบบรวมกลุ่มและไม่ระบุตัวตน
**Cookie เพื่อการทำงาน (Functional Cookies):**
- จดจำการตั้งค่าของท่าน
- ให้องค์ประกอบที่เป็นส่วนตัวมากขึ้น
**Cookie เพื่อการตลาด (Marketing Cookies):**
- ติดตามกิจกรรมการท่องเว็บ
- ใช้เพื่อแสดงโฆษณาที่เกี่ยวข้อง
- แบ่งปันข้อมูลกับบุคคลที่สาม
### 8.3 การจัดการ Cookie
ท่านสามารถจัดการ Cookie ได้โดย:
- **การตั้งค่าเบราว์เซอร์:** ปิดการใช้งาน Cookie ทั้งหมดหรือบางประเภท
- **การตั้งค่า Cookie ของเรา:** เลือกประเภท Cookie ที่ท่านยินยอม
- **เครื่องมือของบุคคลที่สาม:** เช่น Google Analytics Opt-out
### 8.4 ผลกระทบจากการปิด Cookie
หากท่านปิดการใช้งาน Cookie:
- ฟีเจอร์บางอย่างของเว็บไซต์อาจไม่ทำงาน
- ท่านอาจไม่สามารถเข้าสู่ระบบได้
- การตั้งค่าของท่านอาจไม่ถูกจดจำ
## 9. ความปลอดภัยของข้อมูล
เราใช้มาตรการรักษาความปลอดภัยที่เหมาะสมเพื่อคุ้มครองข้อมูลส่วนบุคคลของท่าน:
### 9.1 มาตรการทางเทคนิค
- **การเข้ารหัส:** ข้อมูลถูกเข้ารหัสระหว่างการส่ง (SSL/TLS)
- **การควบคุมการเข้าถึง:** จำกัดการเข้าถึงข้อมูลเฉพาะผู้ที่จำเป็น
- **Firewall:** ป้องกันการเข้าถึงโดยไม่ได้รับอนุญาต
- **การตรวจจับการบุกรุก:** ตรวจสอบกิจกรรมที่ผิดปกติ
### 9.2 มาตรการทางองค์กร
- **นโยบายความปลอดภัย:** นโยบายและขั้นตอนที่ชัดเจน
- **การฝึกอบรม:** พนักงานได้รับการฝึกอบรมเรื่องความปลอดภัยของข้อมูล
- **การตรวจสอบ:** ทบทวนและปรับปรุงมาตรการอย่างสม่ำเสมอ
- **การจัดการผู้ให้บริการ:** ประเมินความปลอดภัยของผู้ให้บริการ
### 9.3 มาตรการทางกายภาพ
- **การควบคุมการเข้าถึง:** จำกัดการเข้าถึงศูนย์ข้อมูล
- **การป้องกันสิ่งแวดล้อม:** ระบบป้องกันอัคคีภัยและน้ำท่วม
- **การทำลายสื่อ:** ทำลายสื่อเก็บข้อมูลอย่างปลอดภัย
### 9.4 การแจ้งเหตุละเมิดข้อมูล
ในกรณีที่มีการละเมิดข้อมูลส่วนบุคคล เราจะ:
- แจ้งสำนักงานคณะกรรมการคุ้มครองข้อมูลส่วนบุคคลภายใน 72 ชั่วโมง
- แจ้งให้ท่านทราบหากมีความเสี่ยงสูงต่อสิทธิและเสรีภาพของท่าน
- ดำเนินการเพื่อลดผลกระทบ
## 10. การโอนข้อมูลข้ามพรมแดน
เราอาจโอนข้อมูลส่วนบุคคลของท่านไปยังประเทศนอกประเทศไทย:
### 10.1 ประเทศปลายทาง
ข้อมูลอาจถูกโอนไปยัง:
- ประเทศที่มีมาตรฐานการคุ้มครองข้อมูลที่เพียงพอ
- ประเทศที่มีมาตรการคุ้มครองที่เหมาะสม
- ประเทศที่กฎหมายกำหนด
### 10.2 มาตรการคุ้มครอง
การโอนข้อมูลข้ามพรมแดนอยู่ภายใต้:
- มาตรฐานข้อบทเชิงสัญญา (Standard Contractual Clauses)
- กฎบัตรบริษัท (Binding Corporate Rules)
- การรับรองมาตรฐาน (Certification)
## 11. เด็กและเยาวชน
### 11.1 อายุขั้นต่ำ
เว็บไซต์ของเราไม่ได้ออกแบบมาสำหรับเด็กอายุต่ำกว่า 20 ปี:
- เราไม่เก็บรวบรวมข้อมูลจากเด็กโดยรู้เท่าไม่ถึงการณ์
- หากท่านอายุต่ำกว่า 20 ปี กรุณาอย่าให้ข้อมูลส่วนบุคคล
### 11.2 ความยินยอมจากผู้ปกครอง
หากเราทราบ bahwaเราเก็บรวบรวมข้อมูลจากเด็กอายุต่ำกว่า 20 ปี:
- เราจะขอความยินยอมจากผู้ปกครอง
- หากไม่ได้รับความยินยอม เราจะลบข้อมูลดังกล่าว
## 12. การเปลี่ยนแปลงนโยบายความเป็นส่วนตัว
เราอาจอัปเดตนโยบายความเป็นส่วนตัวนี้เป็นครั้งคราว:
### 12.1 การแจ้งการเปลี่ยนแปลง
เราจะแจ้งท่านเกี่ยวกับการเปลี่ยนแปลงโดย:
- โพสต์นโยบายที่อัปเดตบนเว็บไซต์
- ส่งอีเมลแจ้งให้ทราบ
- แสดงประกาศบนเว็บไซต์
### 12.2 การยอมรับการเปลี่ยนแปลง
การใช้งานเว็บไซต์ของท่านหลังจากการเปลี่ยนแปลงแสดงว่าท่านยอมรับนโยบายที่อัปเดต:
- หากท่านไม่เห็นด้วย กรุณาหยุดใช้งานเว็บไซต์
- ท่านมีสิทธิถอนความยินยอมหรือลบบัญชี
## 13. การติดต่อ
หากท่านมีคำถามหรือข้อกังวลเกี่ยวกับนโยบายความเป็นส่วนตัว:
### 13.1 เจ้าหน้าที่คุ้มครองข้อมูลส่วนบุคคล (DPO)
**ชื่อ:** [DPO_NAME]
**อีเมล:** [DPO_EMAIL]
**โทรศัพท์:** [DPO_PHONE]
**ที่อยู่:** [COMPANY_ADDRESS]
### 13.2 ช่องทางการติดต่ออื่นๆ
**แบบฟอร์มติดต่อ:** [CONTACT_FORM_URL]
**อีเมล:** [CONTACT_EMAIL]
**โทรศัพท์:** [CONTACT_PHONE]
**ที่อยู่:** [COMPANY_ADDRESS]
### 13.3 หน่วยงานกำกับดูแล
หากท่านไม่พอใจกับการตอบสนองของเรา ท่านสามารถติดต่อ:
**สำนักงานคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล (สคส.)**
ที่อยู่: 120 ถนนแจ้งวัฒนะ แขวงทุ่งสองห้อง เขตหลักสี่ กรุงเทพมหานคร 10210
โทรศัพท์: 0-2141-6900
อีเมล: ocppd@pdpc.or.th
เว็บไซต์: www.pdpc.or.th
## 14. คำจำกัดความ
**"ข้อมูลส่วนบุคคล"** หมายถึง ข้อมูลเกี่ยวกับบุคคลซึ่งทำให้สามารถระบุตัวตนของบุคคลนั้นได้ ไม่ว่าทางตรงหรือทางอ้อม
**"การประมวลผล"** หมายถึง การเก็บรวบรวม ใช้ เปิดเผย ส่งต่อ ปรับเปลี่ยน เปรียบเทียบ ทำลาย หรือการดำเนินการใดๆ กับข้อมูลส่วนบุคคล
**"ผู้ควบคุมข้อมูล"** หมายถึง บุคคลหรือนิติบุคคลซึ่งมีอำนาจหน้าที่ตัดสินใจเกี่ยวกับการเก็บรวบรวม ใช้ หรือเปิดเผยข้อมูลส่วนบุคคล
**"ผู้ประมวลผลข้อมูล"** หมายถึง บุคคลหรือนิติบุคคลซึ่งดำเนินการเกี่ยวกับการเก็บรวบรวม ใช้ หรือเปิดเผยข้อมูลส่วนบุคคลตามคำสั่งหรือในนามของผู้ควบคุมข้อมูล
## 15. กฎหมายที่ใช้บังคับ
นโยบายความเป็นส่วนตัวนี้ตีความและบังคับใช้ตามกฎหมายแห่งราชอาณาจักรไทย:
- พระราชบัญญัติคุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562
- พระราชบัญญัติว่าด้วยการกระทำความผิดเกี่ยวกับคอมพิวเตอร์
- กฎหมายคุ้มครองผู้บริโภค
## 16. การแยกความมีผลบังคับใช้
หากข้อกำหนดใดในนโยบายนี้ถูกพิจารณาว่าเป็นโมฆะหรือบังคับไม่ได้:
- ข้อกำหนดดังกล่าวจะถูกตัดออก
- ข้อกำหนดที่เหลือจะยังคงมีผลบังคับใช้เต็มที่
---
**ลงชื่อ:** _________________________
**ชื่อ:** [AUTHORIZED_NAME]
**ตำแหน่ง:** [AUTHORIZED_TITLE]
**วันที่:** [DATE]
**บริษัท [COMPANY_NAME]**
---
*เอกสารนี้เป็นเอกสารทางกฎหมาย หากท่านมีข้อสงสัย กรุณาปรึกษาที่ปรึกษากฎหมาย*

View File

@@ -1,416 +0,0 @@
# เงื่อนไขการให้บริการ (Terms of Service)
**ชื่อเว็บไซต์:** [WEBSITE_NAME]
**เว็บไซต์:** [WEBSITE_URL]
**มีผลบังคับใช้วันที่:** [DATE]
**แก้ไขล่าสุด:** [DATE]
## 1. การยอมรับเงื่อนไข
### 1.1 ข้อตกลง
ด้วยการเข้าถึงและใช้งานเว็บไซต์ [WEBSITE_URL] ("เว็บไซต์") ของบริษัท [COMPANY_NAME] ("เรา", "ของเรา" หรือ "บริษัท") ท่าน ("ผู้ใช้", "ท่าน" หรือ "ของคุณ") ยอมรับและตกลงที่จะถูกผูกมัดด้วยเงื่อนไขการให้บริการฉบับนี้ ("เงื่อนไข")
### 1.2 การแก้ไขเงื่อนไข
เราขอสงวนสิทธิในการแก้ไขเงื่อนไขนี้เมื่อใดก็ได้:
- การแก้ไขจะมีผลทันทีเมื่อโพสต์บนเว็บไซต์
- ท่านควรตรวจสอบเงื่อนไขนี้เป็นประจำ
- การใช้งานเว็บไซต์ต่อเนื่องแสดงว่าท่านยอมรับการแก้ไข
### 1.3 อายุขั้นต่ำ
ท่านต้องมีอายุไม่ต่ำกว่า 20 ปีบริบูรณ์เพื่อใช้งานเว็บไซต์:
- หากท่านอายุต่ำกว่า 20 ปี ท่านต้องได้รับความยินยอมจากผู้ปกครอง
- ผู้ปกครองต้องตกลงที่จะผูกพันด้วยเงื่อนไขนี้
## 2. บริการของเรา
### 2.1 คำอธิบายบริการ
เว็บไซต์ของเราให้บริการ:
- [SERVICE_DESCRIPTION]
- ข้อมูลและเนื้อหาเกี่ยวกับ [TOPIC]
- เครื่องมือและฟีเจอร์ต่างๆ
### 2.2 การเปลี่ยนแปลงบริการ
เราขอสงวนสิทธิในการ:
- เพิ่ม ลบ หรือแก้ไขฟีเจอร์ของบริการ
- ระงับหรือยุติบริการชั่วคราวหรือถาวร
- จำกัดการเข้าถึงบางส่วนหรือทั้งหมดของบริการ
### 2.3 ความพร้อมของบริการ
เราพยายามให้บริการอย่างต่อเนื่อง แต่:
- เราไม่รับประกันว่าบริการจะปราศจากข้อผิดพลาด
- เราไม่รับผิดชอบต่อ downtime ที่ไม่ได้ตั้งใจ
- เราขอสงวนสิทธิในการหยุดให้บริการโดยไม่แจ้งล่วงหน้า
## 3. บัญชีผู้ใช้
### 3.1 การสร้างบัญชี
เพื่อใช้งานบริการบางอย่าง ท่านต้องสร้างบัญชีผู้ใช้:
- ท่านต้องให้ข้อมูลที่ถูกต้อง ครบถ้วน และทันสมัย
- ท่านต้องรักษารหัสผ่านให้เป็นความลับ
- ท่านรับผิดชอบต่อทุกกิจกรรมที่เกิดขึ้นภายใต้บัญชีของท่าน
### 3.2 ข้อกำหนดของบัญชี
- หนึ่งคนต่อหนึ่งบัญชีเท่านั้น
- ห้ามแบ่งปันบัญชีกับผู้อื่น
- ห้ามใช้ชื่อบัญชีที่ผิดกฎหมายหรือละเมิดสิทธิผู้อื่น
### 3.3 การระงับบัญชี
เราขอสงวนสิทธิในการระงับหรือลบบัญชีของท่านหาก:
- ท่านละเมิดเงื่อนไขนี้
- มีการ_activity_ที่น่าสงสัยหรือเป็นอันตราย
- มีการร้องเรียนจากผู้ใช้รายอื่น
- ตามข้อกำหนดของกฎหมาย
### 3.4 การลบบัญชี
ท่านสามารถลบบัญชีของท่านเมื่อใดก็ได้:
- ติดต่อเราที่ [CONTACT_EMAIL]
- ข้อมูลบางอย่างอาจถูกเก็บไว้ตามข้อกำหนดของกฎหมาย
- การลบบัญชีไม่สามารถย้อนกลับได้
## 4. ความเป็นเจ้าของทรัพย์สินทางปัญญา
### 4.1 สิทธิของเรา
เว็บไซต์และเนื้อหาทั้งหมดเป็นทรัพย์สินของเราหรือผู้ให้ใบอนุญาต:
- เนื้อหา ข้อความ กราฟิก โลโก้
- ซอฟต์แวร์ โค้ด ฐานข้อมูล
- การออกแบบ เลย์เอาต์
### 4.2 เครื่องหมายการค้า
เครื่องหมายการค้า โลโก้ และชื่อบริการเป็นเครื่องหมายการค้าของเรา:
- ห้ามใช้โดยไม่ได้รับอนุญาตเป็นลายลักษณ์อักษร
- การใช้โดยไม่ได้รับอนุญาตอาจเป็นการละเมิดกฎหมาย
### 4.3 สิทธิของท่าน
ท่าน retainsสิทธิในเนื้อหาที่ท่านส่งมา:
- ท่านยังคงเป็นเจ้าของเนื้อหาของท่าน
- ท่านให้ใบอนุญาตแก่เราในการใช้เนื้อหานั้น
- ท่านรับประกันว่ามีสิทธิในการให้ใบอนุญาต
### 4.4 ใบอนุญาตการใช้งาน
ท่านได้รับใบอนุญาตที่เพิกถอนได้ ไม่เฉพาะเจาะจง ไม่สามารถโอนย้ายได้:
- เข้าถึงและใช้งานบริการเพื่อวัตถุประสงค์ส่วนบุคคล
- ห้ามใช้เพื่อวัตถุประสงค์เชิงพาณิชย์โดยไม่ได้รับอนุญาต
- ห้ามดัดแปลง แก้ไข หรือสร้างงานดัดแปลง
## 5. ข้อห้ามในการใช้งาน
### 5.1 กิจกรรมที่ต้องห้าม
ท่านตกลงที่จะไม่:
**กิจกรรมที่ผิดกฎหมาย:**
- ใช้เว็บไซต์เพื่อกิจกรรมที่ผิดกฎหมาย
- ละเมิดสิทธิทางปัญญาของผู้อื่น
- ละเมิดความเป็นส่วนตัวของผู้อื่น
- ส่งเนื้อหาที่ผิดกฎหมายหรือเป็นอันตราย
**กิจกรรมที่เป็นอันตราย:**
- เผยแพร่ไวรัส มัลแวร์ หรือโค้ดที่เป็นอันตราย
- พยายามเข้าถึงระบบโดยไม่ได้รับอนุญาต
- รบกวนหรือขัดขวางการทำงานของเว็บไซต์
- ดำเนินการ reverse engineering ของซอฟต์แวร์
**กิจกรรมที่ละเมิดสิทธิ:**
- ละเมิดลิขสิทธิ์ เครื่องหมายการค้า หรือสิทธิอื่นๆ
- ใช้ข้อมูลส่วนบุคคลของผู้อื่นโดยไม่ได้รับอนุญาต
- ส่งสแปมหรือข้อความเชิงพาณิชย์ที่ไม่พึงประสงค์
- ปลอมแปลงตัวตนหรือแหล่งที่มาของเนื้อหา
**กิจกรรมที่ผิดจริยธรรม:**
- ส่งเนื้อหาที่หยาบคาย อนาจาร หรือผิดศีลธรรม
- ส่งเสริมการเลือกปฏิบัติหรือความเกลียดชัง
- ส่งเสริมความรุนแรงหรือการทำร้ายตนเอง
- ส่งเสริมการพนันหรือยาเสพติดที่ผิดกฎหมาย
### 5.2 ผลของการละเมิด
หากท่านละเมิดข้อห้าม:
- บัญชีของท่านอาจถูกระงับหรือลบ
- เราอาจดำเนินการทางกฎหมาย
- เราอาจแจ้งหน่วยงานบังคับใช้กฎหมาย
## 6. เนื้อหาที่ผู้ใช้ส่ง
### 6.1 คำจำกัดความ
"เนื้อหาที่ผู้ใช้ส่ง" หมายถึงเนื้อหาใดๆ ที่ท่านส่ง โพสต์ หรือแสดงบนเว็บไซต์:
- ความคิดเห็น รีวิว
- รูปภาพ วิดีโอ
- ข้อความ ไฟล์
### 6.2 ใบอนุญาต
โดยส่งเนื้อหา ท่านให้ใบอนุญาตแก่เรา:
- ใบอนุญาตทั่วโลก ไม่เฉพาะเจาะจง ย่อยได้
- สิทธิในการใช้ ทำซ้ำ ดัดแปลง เผยแพร่
- สิทธิในการแสดงเนื้อหา
- ใบอนุญาตนี้ไม่มีค่าตอบแทน
### 6.3 ความรับผิดชอบของท่าน
ท่านรับผิดชอบเนื้อหาที่ท่านส่ง:
- ท่านรับประกันว่ามีสิทธิในการส่งเนื้อหา
- เนื้อหาไม่ละเมิดสิทธิของผู้อื่น
- เนื้อหาไม่ผิดกฎหมายหรือเป็นอันตราย
### 6.4 การตรวจสอบเนื้อหา
เราขอสงวนสิทธิในการ:
- ตรวจสอบเนื้อหาที่ส่งมา
- ลบเนื้อหาที่ละเมิดเงื่อนไข
- รายงานกิจกรรมที่ผิดกฎหมายต่อเจ้าหน้าที่
### 6.5 การตอบสนองต่อการละเมิด
หากท่านเชื่อว่ามีการละเมิดลิขสิทธิ์:
- แจ้งเราที่ [CONTACT_EMAIL]
- ให้ข้อมูลการละเมิดโดยละเอียด
- เราจะดำเนินการตาม DMCA และกฎหมายที่เกี่ยวข้อง
## 7. การชำระเงิน
### 7.1 ราคาและค่าธรรมเนียม
- ราคาทั้งหมดแสดงเป็นเงินบาทไทย (THB)
- ราคานี้รวม/ไม่รวมภาษีมูลค่าเพิ่ม
- เราขอสงวนสิทธิในการเปลี่ยนราคาเมื่อใดก็ได้
### 7.2 การชำระเงิน
การชำระเงินต้องชำระล่วงหน้า:
- เรายอมรับการชำระเงินผ่าน [PAYMENT_METHODS]
- การชำระเงินจะประมวลผลโดยบุคคลที่สาม
- ท่านต้องให้ข้อมูลการชำระเงินที่ถูกต้อง
### 7.3 การคืนเงิน
นโยบายการคืนเงิน:
- [REFUND_POLICY_DETAILS]
- คำขอคืนเงินต้องส่งภายใน [X] วัน
- การคืนเงินจะประมวลผลภายใน [X] วันทำการ
### 7.4 การต่ออายุอัตโนมัติ
หากบริการมีการต่ออายุอัตโนมัติ:
- ท่านจะได้รับแจ้งก่อนการต่ออายุ
- ท่านสามารถยกเลิกการต่ออายุเมื่อใดก็ได้
- การยกเลิกจะมีผลหลังระยะเวลาปัจจุบันสิ้นสุด
## 8. การปฏิเสธความรับผิดชอบ
### 8.1 "ตามที่เป็น"
บริการให้บริการ "ตามที่เป็น" และ "ตามที่มี":
- เราไม่รับประกันว่าบริการจะปราศจากข้อผิดพลาด
- เราไม่รับประกันว่าบริการจะตรงตามความต้องการของท่าน
- เราไม่รับประกันความถูกต้องของข้อมูล
### 8.2 การปฏิเสธความรับผิดชอบ
ภายใต้ขอบเขตที่กฎหมายอนุญาต เราปฏิเสธความรับผิดชอบ:
- ความเสียหายโดยตรง ทางอ้อม โดยบังเอิญ หรือเชิงลงโทษ
- การสูญเสียข้อมูลหรือข้อมูล
- การหยุดชะงักของธุรกิจ
- ความเสียหายอื่นๆ
### 8.3 ข้อจำกัดความรับผิด
ความรับผิดรวมของเราจะไม่เกิน:
- จำนวนที่ท่านจ่ายให้เราในช่วง 12 เดือนที่ผ่านมา
- หรือ 1,000 บาท แล้วแต่จำนวนใดมากกว่า
### 8.4 ข้อยกเว้น
ข้อจำกัดบางอย่างไม่ใช้บังคับกับ:
- การเสียชีวิตหรือการบาดเจ็บส่วนบุคคล
- การฉ้อโกงหรือการแสดงโดยประมาทเลินเล่ออย่างร้ายแรง
- หน้าที่ที่ไม่สามารถถูกจำกัดตามกฎหมาย
## 9. การชดเชย
### 9.1 ข้อตกลงการชดเชย
ท่านตกลงที่จะชดใช้และปกป้องเราจาก:
- การเรียกร้อง ค่าเสียหาย ค่าใช้จ่าย
- ที่เกิดจากการใช้งานเว็บไซต์ของท่าน
- ที่เกิดจากการละเมิดเงื่อนไขนี้
- ที่เกิดจากการละเมิดสิทธิของผู้อื่น
### 9.2 ขั้นตอนการชดเชย
เมื่อได้รับการเรียกร้อง:
- เราจะแจ้งท่านเป็นลายลักษณ์อักษร
- ท่านจะมีสิทธิในการป้องกัน
- เราจะร่วมมือในการป้องกัน
## 10. ความเป็นส่วนตัว
### 10.1 นโยบายความเป็นส่วนตัว
การใช้ข้อมูลส่วนบุคคลอยู่ภายใต้นโยบายความเป็นส่วนตัว:
- อ่านนโยบายความเป็นส่วนตัวของเรา
- นโยบายความเป็นส่วนตัวเป็นส่วนหนึ่งของเงื่อนไขนี้
- ในกรณีที่มีความขัดแย้ง เงื่อนไขนี้จะมีผลบังคับใช้
### 10.2 Cookie
เราใช้ Cookie และเทคโนโลยีการติดตาม:
- อ่านนโยบาย Cookie ของเรา
- ท่านสามารถจัดการการตั้งค่า Cookie ได้
- การปิดการใช้งาน Cookie อาจจำกัดการทำงานของเว็บไซต์
## 11. ลิงก์ไปยังเว็บไซต์ภายนอก
### 11.1 ลิงก์ของบุคคลที่สาม
เว็บไซต์อาจมีลิงก์ไปยังเว็บไซต์ของบุคคลที่สาม:
- เราไม่ควบคุมเว็บไซต์เหล่านั้น
- เราไม่รับผิดชอบเนื้อหาหรือการปฏิบัติของเว็บไซต์เหล่านั้น
- การเข้าถึงเว็บไซต์เหล่านั้นเป็นความเสี่ยงของท่าน
### 11.2 การโฆษณา
เว็บไซต์อาจมีโฆษณาของบุคคลที่สาม:
- เราไม่รับผิดชอบผลิตภัณฑ์หรือบริการที่โฆษณา
- ธุรกรรมกับเจ้าของโฆษณาอยู่ระหว่างท่านและเจ้าของโฆษณา
- เราไม่ตรวจสอบหรือรับรองการโฆษณา
## 12. การยุติบริการ
### 12.1 การยุติโดยท่าน
ท่านสามารถยุติการใช้งานเว็บไซต์เมื่อใดก็ได้:
- หยุดใช้งานเว็บไซต์
- ลบบัญชีของท่าน
- ส่งคำขอเป็นลายลักษณ์อักษร
### 12.2 การยุติโดยเรา
เราขอสงวนสิทธิในการยุติการเข้าถึงของท่าน:
- โดยไม่แจ้งล่วงหน้า
- ด้วยเหตุผลใดๆ หรือไม่มีเหตุผล
- ทันทีที่มีผล
### 12.3 ผลของการยุติ
เมื่อการเข้าถึงถูกยุติ:
- สิทธิ์ในการใช้งานเว็บไซต์สิ้นสุดลง
- ท่านต้องหยุดใช้งานเว็บไซต์ทันที
- ข้อกำหนดบางประการยังคงมีผล (ดูข้อ 15)
## 13. กฎหมายที่ใช้บังคับ
### 13.1 กฎหมายไทย
เงื่อนไขนี้ถูกควบคุมและตีความตามกฎหมายแห่งราชอาณาจักรไทย:
- พระราชบัญญัติคุ้มครองผู้บริโภค
- พระราชบัญญัติว่าด้วยการกระทำความผิดเกี่ยวกับคอมพิวเตอร์
- พระราชบัญญัติลิขสิทธิ์
- กฎหมายที่เกี่ยวข้องอื่นๆ
### 13.2 เขตอำนาจศาล
ข้อพิพาทใดๆ อยู่ภายใต้เขตอำนาจศาลของ:
- ศาลไทย
- กรุงเทพมหานคร
- หรือศาลที่มีเขตอำนาจ
### 13.3 การระงับข้อพิพาท
ก่อนดำเนินการทางกฎหมาย:
- พยายามเจรจาเพื่อระงับข้อพิพาท
- ใช้เวลา 30 วันในการเจรจา
- หากไม่สำเร็จ จึงดำเนินการทางกฎหมาย
## 14. ข้อกำหนดทั่วไป
### 14.1 การสละสิทธิ
การไม่บังคับใช้สิทธิใดๆ ไม่ถือเป็นการสละสิทธิ:
- การสละสิทธิต้องเป็นลายลักษณ์อักษร
- การสละสิทธิครั้งหนึ่งไม่ถือเป็นการสละสิทธิในอนาคต
### 14.2 การโอนสิทธิ
ท่านไม่สามารถโอนสิทธิหรือหน้าที่ภายใต้เงื่อนไขนี้:
- การโอนที่พยายามทำจะถือเป็นโมฆะ
- เราสามารถโอนสิทธิของเราได้โดยไม่ต้องแจ้งให้ท่านทราบ
### 14.3 ความสัมพันธ์ระหว่างคู่สัญญา
เงื่อนไขนี้ไม่สร้างความสัมพันธ์:
- ไม่มีความสัมพันธ์การจ้างงาน
- ไม่มีความสัมพันธ์หุ้นส่วน
- ไม่มีความสัมพันธ์ร่วมทุน
### 14.4 การแยกความมีผลบังคับใช้
หากข้อกำหนดใดถูกพิจารณาว่าเป็นโมฆะ:
- ข้อกำหนดนั้นจะถูกตัดออก
- ข้อกำหนดที่เหลือจะยังคงมีผลบังคับใช้เต็มที่
### 14.5 ข้อกำหนดทั้งหมด
เงื่อนไขนี้เป็นข้อตกลงทั้งหมดระหว่างท่านและเรา:
- แทนที่ข้อตกลงหรือความเข้าใจก่อนหน้าทั้งหมด
- ไม่ว่าจะด้วยลายลักษณ์อักษรหรือด้วยวาจา
- ไม่มีการแก้ไขด้วยวาจามีผลบังคับใช้
## 15. ข้อกำหนดที่ยังคงมีผล
ข้อกำหนดดังต่อไปนี้จะยังคงมีผลหลังการยุติ:
- ความเป็นเจ้าของทรัพย์สินทางปัญญา
- การปฏิเสธความรับผิดชอบ
- ข้อจำกัดความรับผิด
- การชดเชย
- กฎหมายที่ใช้บังคับ
## 16. การติดต่อ
หากท่านมีคำถามเกี่ยวกับเงื่อนไขนี้:
**อีเมล:** [CONTACT_EMAIL]
**โทรศัพท์:** [CONTACT_PHONE]
**ที่อยู่:** [COMPANY_ADDRESS]
**แบบฟอร์มติดต่อ:** [CONTACT_FORM_URL]
---
## ภาคผนวก ก: คำจำกัดความ
**"บัญชี"** หมายถึง บัญชีผู้ใช้ที่ท่านสร้างบนเว็บไซต์
**"เนื้อหา"** หมายถึง ข้อมูล ข้อความ กราฟิก ภาพ วิดีโอ ซอฟต์แวร์ หรือวัสดุอื่นๆ
**"เว็บไซต์"** หมายถึง เว็บไซต์ [WEBSITE_URL] และบริการที่เกี่ยวข้องทั้งหมด
**"เรา" "ของเรา"** หมายถึง บริษัท [COMPANY_NAME]
**"ท่าน" "ผู้ใช้"** หมายถึง บุคคลหรือนิติบุคคลที่เข้าถึงหรือใช้งานเว็บไซต์
---
**ลงชื่อ:** _________________________
**ชื่อ:** [AUTHORIZED_NAME]
**ตำแหน่ง:** [AUTHORIZED_TITLE]
**วันที่:** [DATE]
**บริษัท [COMPANY_NAME]**
---
*เอกสารนี้เป็นเอกสารทางกฎหมาย หากท่านมีข้อสงสัย กรุณาปรึกษาที่ปรึกษากฎหมาย*

View File

@@ -1,213 +0,0 @@
#!/usr/bin/env python3
"""
Umami Integration Helper
Integrates Umami Analytics into website creation workflow.
Auto-creates Umami website and adds tracking to Astro layout.
"""
import os
import sys
import requests
from typing import Dict, Optional, Tuple
from datetime import datetime
class UmamiIntegration:
"""Handle Umami website creation and tracking integration"""
def __init__(self, umami_url: str, username: str, password: str):
"""
Initialize Umami integration
Args:
umami_url: Umami instance URL
username: Umami username
password: Umami password
"""
self.umami_url = umami_url.rstrip('/')
self.api_url = f"{self.umami_url}/api"
self.username = username
self.password = password
self.token = None
self.user_id = None
def login(self) -> Tuple[bool, str]:
"""Login to Umami"""
try:
url = f"{self.api_url}/auth/login"
data = {'username': self.username, 'password': self.password}
response = requests.post(url, json=data, timeout=10)
response.raise_for_status()
result = response.json()
if 'token' in result:
self.token = result['token']
self.user_id = result.get('user', {}).get('id')
return True, "Login successful"
else:
return False, "No token in response"
except requests.exceptions.RequestException as e:
return False, f"Login failed: {str(e)}"
def create_website(self, website_name: str, website_domain: str) -> Tuple[bool, Dict]:
"""
Create Umami website
Args:
website_name: Name for Umami website
website_domain: Website domain
Returns:
(success, result_dict)
"""
# Login first
success, message = self.login()
if not success:
return False, {'error': message}
try:
# Create website
url = f"{self.api_url}/websites"
data = {'name': website_name, 'domain': website_domain}
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json'
}
response = requests.post(url, json=data, headers=headers, timeout=10)
response.raise_for_status()
result = response.json()
return True, {
'website_id': result.get('id'),
'name': result.get('name'),
'domain': result.get('domain'),
'tracking_script': self._get_tracking_script(result.get('id'))
}
except requests.exceptions.RequestException as e:
return False, {'error': f"Create website failed: {str(e)}"}
def _get_tracking_script(self, website_id: str) -> str:
"""Generate tracking script HTML"""
return f'<script defer src="{self.umami_url}/script.js" data-website-id="{website_id}"></script>'
def add_tracking_to_layout(self, layout_file: str, website_id: str) -> Tuple[bool, str]:
"""
Add Umami tracking to Astro layout
Args:
layout_file: Path to Astro layout file
website_id: Umami website ID
Returns:
(success, message)
"""
try:
if not os.path.exists(layout_file):
return False, f"Layout file not found: {layout_file}"
# Read layout
with open(layout_file, 'r', encoding='utf-8') as f:
content = f.read()
# Add tracking before </head>
tracking_script = self._get_tracking_script(website_id)
if '</head>' in content:
# Insert before </head>
indent = ' '
content = content.replace(
'</head>',
f'{indent}{tracking_script}\n </head>'
)
else:
# Add at end
content += f'\n{tracking_script}\n'
# Write back
with open(layout_file, 'w', encoding='utf-8') as f:
f.write(content)
return True, f"Tracking added to {layout_file}"
except Exception as e:
return False, f"Failed to add tracking: {str(e)}"
def setup_umami_for_website(
umami_url: str,
username: str,
password: str,
website_name: str,
website_domain: str,
website_repo: str
) -> Tuple[bool, Dict]:
"""
Complete Umami setup for new website
Args:
umami_url: Umami instance URL
username: Umami username
password: Umami password
website_name: Name for website
website_domain: Website domain
website_repo: Path to website repository
Returns:
(success, result_dict)
"""
print(f"\n📈 Setting up Umami Analytics...")
print(f" URL: {umami_url}")
print(f" Website: {website_name}")
# Initialize integration
umami = UmamiIntegration(umami_url, username, password)
# Step 1: Create Umami website
print(f" Creating Umami website...")
success, result = umami.create_website(website_name, website_domain)
if not success:
print(f" ✗ Failed: {result.get('error', 'Unknown error')}")
return False, result
website_id = result.get('website_id')
print(f" ✓ Created: {website_id}")
# Step 2: Add tracking to Astro layout
print(f" Adding tracking to website...")
# Find layout file
layout_paths = [
os.path.join(website_repo, 'src/layouts/BaseHead.astro'),
os.path.join(website_repo, 'src/layouts/Layout.astro'),
os.path.join(website_repo, 'src/pages/_document.tsx')
]
layout_file = None
for path in layout_paths:
if os.path.exists(path):
layout_file = path
break
if layout_file:
success, message = umami.add_tracking_to_layout(layout_file, website_id)
if success:
print(f"{message}")
else:
print(f"{message}")
else:
print(f" ⚠ No layout file found - manual tracking setup required")
return True, {
'website_id': website_id,
'name': website_name,
'domain': website_domain,
'tracking_script': result.get('tracking_script'),
'layout_updated': layout_file is not None
}