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:
19
skills/thai-frontend-dev/scripts/.env.example
Normal file
19
skills/thai-frontend-dev/scripts/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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"
|
||||
776
skills/thai-frontend-dev/scripts/create_astro_website.py
Normal file
776
skills/thai-frontend-dev/scripts/create_astro_website.py
Normal file
@@ -0,0 +1,776 @@
|
||||
#!/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()
|
||||
561
skills/thai-frontend-dev/scripts/migrate_existing_website.py
Normal file
561
skills/thai-frontend-dev/scripts/migrate_existing_website.py
Normal file
@@ -0,0 +1,561 @@
|
||||
#!/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()
|
||||
1284
skills/thai-frontend-dev/scripts/refactor_existing_website.py
Normal file
1284
skills/thai-frontend-dev/scripts/refactor_existing_website.py
Normal file
File diff suppressed because it is too large
Load Diff
1
skills/thai-frontend-dev/scripts/requirements.txt
Normal file
1
skills/thai-frontend-dev/scripts/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests>=2.28.0
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
// 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>
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
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>
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
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>
|
||||
10
skills/thai-frontend-dev/scripts/templates/icons/line.svg
Normal file
10
skills/thai-frontend-dev/scripts/templates/icons/line.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,190 @@
|
||||
---
|
||||
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>
|
||||
183
skills/thai-frontend-dev/scripts/templates/pages/index.astro
Normal file
183
skills/thai-frontend-dev/scripts/templates/pages/index.astro
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
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>
|
||||
298
skills/thai-frontend-dev/scripts/templates/styles/global.css
Normal file
298
skills/thai-frontend-dev/scripts/templates/styles/global.css
Normal file
@@ -0,0 +1,298 @@
|
||||
/* 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; }
|
||||
@@ -0,0 +1,423 @@
|
||||
# นโยบายความเป็นส่วนตัว (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]**
|
||||
|
||||
---
|
||||
|
||||
*เอกสารนี้เป็นเอกสารทางกฎหมาย หากท่านมีข้อสงสัย กรุณาปรึกษาที่ปรึกษากฎหมาย*
|
||||
@@ -0,0 +1,416 @@
|
||||
# เงื่อนไขการให้บริการ (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]**
|
||||
|
||||
---
|
||||
|
||||
*เอกสารนี้เป็นเอกสารทางกฎหมาย หากท่านมีข้อสงสัย กรุณาปรึกษาที่ปรึกษากฎหมาย*
|
||||
213
skills/thai-frontend-dev/scripts/umami_integration.py
Normal file
213
skills/thai-frontend-dev/scripts/umami_integration.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/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
|
||||
}
|
||||
Reference in New Issue
Block a user