feat: add contact SMTP API, pricing section, and nodemailer
Some checks failed
PR Sweep / Sweep Open PRs (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Validate Plugins (push) Has been cancelled
CI / Smoke Tests (push) Has been cancelled
CI / Integration Tests (push) Has been cancelled
CI / Browser Tests (push) Has been cancelled
CI / E2E tests (1/8) (push) Has been cancelled
CI / E2E tests (2/8) (push) Has been cancelled
CI / E2E tests (3/8) (push) Has been cancelled
CI / E2E tests (4/8) (push) Has been cancelled
CI / E2E tests (5/8) (push) Has been cancelled
CI / E2E tests (6/8) (push) Has been cancelled
CI / E2E tests (7/8) (push) Has been cancelled
CI / E2E tests (8/8) (push) Has been cancelled
Seed Marketplace Plugins / Seed Plugins (push) Has been cancelled
Format / Format (push) Has been cancelled
Preview Releases / Publish Preview (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
PR Sweep / Sweep Open PRs (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Validate Plugins (push) Has been cancelled
CI / Smoke Tests (push) Has been cancelled
CI / Integration Tests (push) Has been cancelled
CI / Browser Tests (push) Has been cancelled
CI / E2E tests (1/8) (push) Has been cancelled
CI / E2E tests (2/8) (push) Has been cancelled
CI / E2E tests (3/8) (push) Has been cancelled
CI / E2E tests (4/8) (push) Has been cancelled
CI / E2E tests (5/8) (push) Has been cancelled
CI / E2E tests (6/8) (push) Has been cancelled
CI / E2E tests (7/8) (push) Has been cancelled
CI / E2E tests (8/8) (push) Has been cancelled
Seed Marketplace Plugins / Seed Plugins (push) Has been cancelled
Format / Format (push) Has been cancelled
Preview Releases / Publish Preview (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- Replace contact form with AJAX fetch + /api/contact endpoint - Add nodemailer SMTP integration for email sending - Add pricing cards section to web-development.astro - Fix package.json nodemailer catalog entry
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@emdash-cms/template-marketing",
|
||||
"version": "0.0.3",
|
||||
"name": "moreminimore",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"emdash": {
|
||||
@@ -18,9 +18,11 @@
|
||||
"dependencies": {
|
||||
"@astrojs/node": "catalog:",
|
||||
"@astrojs/react": "catalog:",
|
||||
"@moreminimore/consent": "file:./plugins/consent",
|
||||
"astro": "catalog:",
|
||||
"better-sqlite3": "catalog:",
|
||||
"emdash": "workspace:*",
|
||||
"nodemailer": "^6.9.16",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
|
||||
216
templates/marketing/src/pages/api/contact.ts
Normal file
216
templates/marketing/src/pages/api/contact.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const data = await request.formData();
|
||||
|
||||
const name = data.get("name")?.toString().trim() || "";
|
||||
const email = data.get("email")?.toString().trim() || "";
|
||||
const phone = data.get("phone")?.toString().trim() || "";
|
||||
const service = data.get("service")?.toString().trim() || "";
|
||||
const message = data.get("message")?.toString().trim() || "";
|
||||
|
||||
// Validation
|
||||
if (!name || !email || !message) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: "กรุณากรอกข้อมูลให้ครบ" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: "รูปแบบอีเมลไม่ถูกต้อง" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Check SMTP config from environment
|
||||
const smtpHost = import.meta.env.SMTP_HOST;
|
||||
const smtpPort = import.meta.env.SMTP_PORT || "587";
|
||||
const smtpUser = import.meta.env.SMTP_USER;
|
||||
const smtpPass = import.meta.env.SMTP_PASS;
|
||||
const smtpFrom = import.meta.env.SMTP_FROM || "noreply@moreminimore.com";
|
||||
const toEmail = import.meta.env.CONTACT_EMAIL || "contact@moreminimore.com";
|
||||
|
||||
const serviceLabel: Record<string, string> = {
|
||||
"web-development": "Web Development",
|
||||
"ai-automation": "AI Automation",
|
||||
"marketing-automation": "Marketing Automation",
|
||||
"tech-consult": "Tech Consult",
|
||||
other: "อื่นๆ",
|
||||
};
|
||||
|
||||
const htmlBody = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="th">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: 'Noto Sans Thai', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 600px; margin: 0 auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
.header { background: #0a0a0a; color: #fed400; padding: 24px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 20px; }
|
||||
.body { padding: 24px; }
|
||||
.field { margin-bottom: 16px; }
|
||||
.label { font-size: 12px; color: #737373; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
||||
.value { font-size: 16px; color: #0a0a0a; }
|
||||
.message-box { background: #f5f5f5; border-left: 4px solid #fed400; padding: 12px 16px; border-radius: 0 8px 8px 0; }
|
||||
.message-box .value { white-space: pre-wrap; }
|
||||
.footer { padding: 16px 24px; border-top: 1px solid #e5e5e5; font-size: 12px; color: #737373; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📬 มีข้อความใหม่จากเว็บไซต์</h1>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field">
|
||||
<div class="label">ชื่อ</div>
|
||||
<div class="value">${name}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="label">อีเมล</div>
|
||||
<div class="value"><a href="mailto:${email}">${email}</a></div>
|
||||
</div>
|
||||
${phone ? `
|
||||
<div class="field">
|
||||
<div class="label">เบอร์โทรศัพท์</div>
|
||||
<div class="value"><a href="tel:${phone}">${phone}</a></div>
|
||||
</div>` : ""}
|
||||
${service ? `
|
||||
<div class="field">
|
||||
<div class="label">บริการที่สนใจ</div>
|
||||
<div class="value">${serviceLabel[service] || service}</div>
|
||||
</div>` : ""}
|
||||
<div class="field">
|
||||
<div class="label">ข้อความ</div>
|
||||
<div class="message-box">
|
||||
<div class="value">${message.replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
ส่งเมื่อ: ${new Date().toLocaleString("th-TH", { timeZone: "Asia/Bangkok" })}<br>
|
||||
MoreminiMore — contact.moreminimore.com
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const textBody = `
|
||||
มีข้อความใหม่จากเว็บไซต์
|
||||
|
||||
ชื่อ: ${name}
|
||||
อีเมล: ${email}
|
||||
${phone ? `เบอร์โทร: ${phone}` : ""}
|
||||
${service ? `บริการ: ${serviceLabel[service] || service}` : ""}
|
||||
|
||||
ข้อความ:
|
||||
${message}
|
||||
|
||||
---
|
||||
ส่งเมื่อ: ${new Date().toLocaleString("th-TH", { timeZone: "Asia/Bangkok" })}
|
||||
MoreminiMore
|
||||
`;
|
||||
|
||||
// Send email if SMTP is configured
|
||||
if (smtpHost && smtpUser && smtpPass) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: Number(smtpPort),
|
||||
secure: Number(smtpPort) === 465,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"MoreminiMore Website" <${smtpFrom}>`,
|
||||
to: toEmail,
|
||||
replyTo: email,
|
||||
subject: `📬 ข้อความใหม่จาก ${name}${service ? ` - ${serviceLabel[service] || service}` : ""}`,
|
||||
text: textBody,
|
||||
html: htmlBody,
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-reply to sender
|
||||
if (smtpHost && smtpUser && smtpPass) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: Number(smtpPort),
|
||||
secure: Number(smtpPort) === 465,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"MoreminiMore" <${smtpFrom}>`,
|
||||
to: email,
|
||||
subject: "📧 เราได้รับข้อความของคุณแล้ว",
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html lang="th">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: 'Noto Sans Thai', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 600px; margin: 0 auto; background: #fff; border-radius: 12px; overflow: hidden; }
|
||||
.header { background: #0a0a0a; color: #fed400; padding: 24px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 20px; }
|
||||
.body { padding: 24px; color: #0a0a0a; }
|
||||
.body p { line-height: 1.7; margin-bottom: 16px; }
|
||||
.highlight { background: #fff4b3; padding: 12px 16px; border-radius: 8px; margin: 16px 0; }
|
||||
.footer { padding: 16px 24px; border-top: 1px solid #e5e5e5; font-size: 12px; color: #737373; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>✨ ขอบคุณที่ติดต่อ MoreminiMore</h1>
|
||||
</div>
|
||||
<div class="body">
|
||||
<p>สวัสดีคุณ ${name},</p>
|
||||
<p>เราได้รับข้อความของคุณแล้ว และจะตอบกลับภายใน 24 ชั่วโมงทำการ</p>
|
||||
<div class="highlight">
|
||||
<strong>ข้อความของคุณ:</strong><br>
|
||||
${message.substring(0, 200)}${message.length > 200 ? "..." : ""}
|
||||
</div>
|
||||
<p>หากมีเรื่องด่วน สามารถติดต่อเราได้โดยตรงที่:</p>
|
||||
<p>
|
||||
📞 080-995-5945<br>
|
||||
📧 contact@moreminimore.com
|
||||
</p>
|
||||
<p>ขอบคุณครับ/ค่ะ<br>MoreminiMore Team</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
บริษัท มอร์มินิมอร์ จำกัด<br>
|
||||
53 หมู่ 1 ต.บ้านแพ้ว อ.บ้านแพ้ว สมุทรสาคร 74120
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: smtpHost ? "ส่งข้อความเรียบร้อยแล้ว เราจะติดต่อกลับภายใน 24 ชั่วโมง" : "บันทึกข้อความเรียบร้อยแล้ว"
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[Contact API] Error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: "เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,250 +1,161 @@
|
||||
---
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import Base from "../layouts/Base.astro";
|
||||
import MarketingBlocks from "../components/MarketingBlocks.astro";
|
||||
|
||||
const { entry: page, cacheHint } = await getEmDashEntry("pages", "contact");
|
||||
|
||||
try {
|
||||
Astro.cache.set(cacheHint);
|
||||
} catch {}
|
||||
|
||||
// Handle form submission
|
||||
// NOTE: This is demo code. For production, add:
|
||||
// - CSRF token validation
|
||||
// - Rate limiting (e.g., via Cloudflare or middleware)
|
||||
// - Actual email sending or webhook integration
|
||||
let formStatus: "idle" | "success" | "error" = "idle";
|
||||
let formMessage = "";
|
||||
|
||||
if (Astro.request.method === "POST") {
|
||||
try {
|
||||
const formData = await Astro.request.formData();
|
||||
const name = formData.get("name")?.toString() || "";
|
||||
const email = formData.get("email")?.toString() || "";
|
||||
const company = formData.get("company")?.toString() || "";
|
||||
const message = formData.get("message")?.toString() || "";
|
||||
|
||||
if (!name || !email || !message) {
|
||||
formStatus = "error";
|
||||
formMessage = "Please fill in all required fields.";
|
||||
} else if (!email.includes("@")) {
|
||||
formStatus = "error";
|
||||
formMessage = "Please enter a valid email address.";
|
||||
} else {
|
||||
// TODO: Replace with actual email/webhook integration
|
||||
console.log("Contact form submission:", {
|
||||
name,
|
||||
email,
|
||||
company,
|
||||
message,
|
||||
});
|
||||
formStatus = "success";
|
||||
formMessage =
|
||||
"Thanks for reaching out! We'll get back to you within 24 hours.";
|
||||
}
|
||||
} catch {
|
||||
formStatus = "error";
|
||||
formMessage = "Something went wrong. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
const pageContent = page?.data.content;
|
||||
---
|
||||
|
||||
<Base
|
||||
title="Contact"
|
||||
description="Have questions? Want a demo? We'd love to hear from you."
|
||||
title="ติดต่อเรา"
|
||||
description="ติดต่อ MoreminiMore สำหรับปรึกษาฟรีเกี่ยวกับเว็บไซต์ AI Chatbot และ Marketing Automation"
|
||||
>
|
||||
{pageContent && <MarketingBlocks value={pageContent} />}
|
||||
|
||||
<section class="contact-form-section section">
|
||||
<section class="contact-hero">
|
||||
<div class="container">
|
||||
<div class="contact-grid">
|
||||
<div class="contact-info">
|
||||
<h2>Talk to our team</h2>
|
||||
<p>Fill out the form and we'll be in touch within 24 hours.</p>
|
||||
<h1>ติดต่อเรา</h1>
|
||||
<p class="lead">พร้อมให้คำปรึกษาฟรี! ติดต่อมาได้เลย</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="contact-methods">
|
||||
<div class="contact-method">
|
||||
<div class="contact-icon">
|
||||
<i class="ph ph-envelope" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="contact-method-content">
|
||||
<h4>Email</h4>
|
||||
<a href="mailto:hello@acme.example">hello@acme.example</a>
|
||||
</div>
|
||||
<section class="section">
|
||||
<div class="container contact-grid">
|
||||
<div class="contact-info">
|
||||
<h2>ช่องทางการติดต่อ</h2>
|
||||
<div class="contact-cards">
|
||||
<div class="contact-card">
|
||||
<i class="ph ph-phone"></i>
|
||||
<div>
|
||||
<h3>โทรศัพท์</h3>
|
||||
<p>080-995-5945</p>
|
||||
</div>
|
||||
<div class="contact-method">
|
||||
<div class="contact-icon">
|
||||
<i class="ph ph-lifebuoy" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="contact-method-content">
|
||||
<h4>Support</h4>
|
||||
<a href="mailto:support@acme.example">support@acme.example</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-card">
|
||||
<i class="ph ph-envelope"></i>
|
||||
<div>
|
||||
<h3>อีเมล</h3>
|
||||
<p>contact@moreminimore.com</p>
|
||||
</div>
|
||||
<div class="contact-method">
|
||||
<div class="contact-icon">
|
||||
<i class="ph ph-currency-dollar" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="contact-method-content">
|
||||
<h4>Sales</h4>
|
||||
<a href="mailto:sales@acme.example">sales@acme.example</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-card">
|
||||
<i class="ph ph-map-pin"></i>
|
||||
<div>
|
||||
<h3>ที่อยู่</h3>
|
||||
<p>53 หมู่ 1 ต.บ้านแพ้ว<br />อ.บ้านแพ้ว สมุทรสาคร 74120</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-card">
|
||||
<i class="ph ph-clock"></i>
|
||||
<div>
|
||||
<h3>เวลาทำการ</h3>
|
||||
<p>จ-ศ: 9:00-18:00<br />ส: 10:00-16:00</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-form-wrapper">
|
||||
{
|
||||
formStatus === "success" ? (
|
||||
<div class="form-success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h3>Message Sent!</h3>
|
||||
<p>{formMessage}</p>
|
||||
<a href="/contact" class="btn btn-secondary">
|
||||
Send another message
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<form method="POST" class="contact-form">
|
||||
{formStatus === "error" && (
|
||||
<div class="form-error">
|
||||
<p>{formMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label for="name">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
placeholder="Your name"
|
||||
autocomplete="name"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="email">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="you@company.com"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="company">Company</label>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
placeholder="Your company"
|
||||
autocomplete="organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="message">Message *</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows="5"
|
||||
placeholder="Tell us about your project or question..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-form-wrapper">
|
||||
<h2>ส่งข้อความถึงเรา</h2>
|
||||
<form class="contact-form" id="contactForm">
|
||||
<div class="form-group">
|
||||
<label for="name">ชื่อของคุณ</label>
|
||||
<input type="text" id="name" name="name" required placeholder="กรอกชื่อของคุณ" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">อีเมล</label>
|
||||
<input type="email" id="email" name="email" required placeholder="your@email.com" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">เบอร์โทรศัพท์ (ไม่บังคับ)</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="080-xxx-xxxx" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service">บริการที่สนใจ</label>
|
||||
<select id="service" name="service">
|
||||
<option value="">เลือกบริการ</option>
|
||||
<option value="web-development">Web Development</option>
|
||||
<option value="ai-automation">AI Automation</option>
|
||||
<option value="marketing-automation">Marketing Automation</option>
|
||||
<option value="tech-consult">Tech Consult</option>
|
||||
<option value="other">อื่นๆ</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">ข้อความ</label>
|
||||
<textarea id="message" name="message" rows="5" required placeholder="บอกเราเกี่ยวกับโปรเจกต์ของคุณ..."></textarea>
|
||||
</div>
|
||||
<div id="formStatus" class="form-status" hidden></div>
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn">
|
||||
ส่งข้อความ
|
||||
<i class="ph ph-paper-plane-right"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.contact-form-section {
|
||||
padding-bottom: var(--spacing-5xl);
|
||||
.contact-hero {
|
||||
background: var(--color-dark);
|
||||
color: #f5f5f5;
|
||||
padding: var(--spacing-5xl) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact-hero h1 {
|
||||
color: #f5f5f5;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.contact-hero .lead {
|
||||
font-size: var(--font-size-xl);
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr;
|
||||
gap: var(--spacing-4xl);
|
||||
align-items: start;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-3xl);
|
||||
}
|
||||
|
||||
.contact-info h2 {
|
||||
@media (min-width: 768px) {
|
||||
.contact-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-info h2,
|
||||
.contact-form-wrapper h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
margin-bottom: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.contact-info > p {
|
||||
color: var(--color-muted);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.contact-methods {
|
||||
.contact-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.contact-method {
|
||||
.contact-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-bg-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
color: white;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-primary),
|
||||
var(--color-accent)
|
||||
);
|
||||
border-radius: var(--radius);
|
||||
.contact-card i {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-method-content h4 {
|
||||
font-size: var(--font-size-sm);
|
||||
.contact-card h3 {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.contact-method-content a {
|
||||
color: var(--color-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.contact-method-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.contact-form-wrapper {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-2xl);
|
||||
.contact-card p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
@@ -253,105 +164,116 @@ const pageContent = page?.data.content;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
.form-group label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-field input,
|
||||
.form-field textarea {
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: var(--spacing-md);
|
||||
font-family: inherit;
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-base);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-field input:focus,
|
||||
.form-field textarea:focus {
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.form-field input::placeholder,
|
||||
.form-field textarea::placeholder {
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.form-field textarea {
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
.contact-form .btn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
padding: var(--spacing-md);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: var(--radius-sm);
|
||||
color: #dc2626;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.form-error {
|
||||
color: #f87171;
|
||||
}
|
||||
.form-status[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-success {
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl);
|
||||
.form-status-success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
background: var(--color-success);
|
||||
border-radius: var(--radius-full);
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
}
|
||||
|
||||
.form-success h3 {
|
||||
font-size: var(--font-size-xl);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.form-success p {
|
||||
color: var(--color-muted);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.form-status-error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("contactForm") as HTMLFormElement | null;
|
||||
const statusEl = document.getElementById("formStatus");
|
||||
const submitBtn = document.getElementById("submitBtn") as HTMLButtonElement | null;
|
||||
|
||||
function showStatus(message: string, type: "success" | "error") {
|
||||
if (!statusEl) return;
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `form-status form-status-${type}`;
|
||||
statusEl.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
function setLoading(loading: boolean) {
|
||||
if (!submitBtn) return;
|
||||
submitBtn.disabled = loading;
|
||||
submitBtn.textContent = loading ? "กำลังส่ง..." : "ส่งข้อความ";
|
||||
const icon = loading ? "" : '<i class="ph ph-paper-plane-right"></i>';
|
||||
if (!loading) {
|
||||
submitBtn.innerHTML = `ส่งข้อความ ${icon}`;
|
||||
}
|
||||
}
|
||||
|
||||
form?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!statusEl) return;
|
||||
|
||||
statusEl.setAttribute("hidden", "");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showStatus(data.message || "ส่งข้อความเรียบร้อยแล้ว!", "success");
|
||||
form.reset();
|
||||
} else {
|
||||
showStatus(data.error || "เกิดข้อผิดพลาด กรุณาลองใหม่", "error");
|
||||
}
|
||||
} catch {
|
||||
showStatus("เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
223
templates/marketing/src/pages/services/web-development.astro
Normal file
223
templates/marketing/src/pages/services/web-development.astro
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import MarketingBlocks from "../../components/MarketingBlocks.astro";
|
||||
|
||||
const { entry, cacheHint } = await getEmDashEntry("pages", "services/web-development");
|
||||
|
||||
try {
|
||||
Astro.cache.set(cacheHint);
|
||||
} catch {}
|
||||
|
||||
const pageContent = entry?.data.content;
|
||||
---
|
||||
|
||||
<Base
|
||||
title="Web Development"
|
||||
description="รับทำเว็บไซต์ด้วย Astro หรือ WordPress เริ่มต้น 5,000-10,000 บาท รวม AI Editor ที่ลูกค้าปรับได้เอง"
|
||||
>
|
||||
{pageContent ? (
|
||||
<MarketingBlocks value={pageContent} />
|
||||
) : (
|
||||
<section class="service-hero" style="--accent: #f59e0b">
|
||||
<div class="container">
|
||||
<span class="service-badge">Web Development</span>
|
||||
<h1>Web Development</h1>
|
||||
<p class="lead">เว็บไซต์ที่ลูกค้าปรับเองได้ด้วย AI Editor พร้อม SEO และ Dark Mode</p>
|
||||
<a href="/contact" class="btn btn-primary btn-lg">ปรึกษาฟรี</a>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2>เทคโนโลยีที่เราใช้</h2>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-card">
|
||||
<h3>Astro</h3>
|
||||
<p>เว็บไซต์เร็ว ปลอดภัย SEO ดี เริ่มต้น 5,000 บาท</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<h3>WordPress</h3>
|
||||
<p>CMS ยอดนิยม ปรับแต่งง่าย เริ่มต้น 10,000 บาท</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<h3>AI Editor</h3>
|
||||
<p>ลูกค้าปรับเนื้อหาได้เองง่ายๆ ด้วย AI</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" style="background: var(--color-bg-subtle);">
|
||||
<div class="container">
|
||||
<div class="pricing-header">
|
||||
<h2>ราคา Web Development</h2>
|
||||
<p class="pricing-sub">เลือกแพ็กเกจที่เหมาะกับธุรกิจคุณ</p>
|
||||
</div>
|
||||
<div class="pricing-grid">
|
||||
<div class="pricing-card">
|
||||
<h3 class="plan-name">Astro Starter</h3>
|
||||
<div class="plan-price">
|
||||
<span class="price-amount">5,000</span>
|
||||
<span class="price-period">บาท</span>
|
||||
</div>
|
||||
<p class="plan-description">เหมาะสำหรับเว็บไซต์พื้นฐาน 5 หน้า</p>
|
||||
<ul class="plan-features">
|
||||
<li><i class="ph ph-check-circle"></i>5 sections</li>
|
||||
<li><i class="ph ph-check-circle"></i>Mobile responsive</li>
|
||||
<li><i class="ph ph-check-circle"></i>AI content editor</li>
|
||||
<li><i class="ph ph-check-circle"></i>SEO พื้นฐาน</li>
|
||||
<li><i class="ph ph-check-circle"></i>1 เดือน support</li>
|
||||
<li><i class="ph ph-check-circle"></i>Hosting หรือ deploy ให้</li>
|
||||
</ul>
|
||||
<a href="/contact" class="btn btn-secondary btn-lg">ติดต่อสอบถาม</a>
|
||||
</div>
|
||||
<div class="pricing-card pricing-card-highlighted">
|
||||
<div class="pricing-badge">ยอดนิยม</div>
|
||||
<h3 class="plan-name">WordPress Starter</h3>
|
||||
<div class="plan-price">
|
||||
<span class="price-amount">10,000</span>
|
||||
<span class="price-period">บาท</span>
|
||||
</div>
|
||||
<p class="plan-description">เหมาะสำหรับเว็บไซต์ที่ต้องการ CMS ยืดหยุ่น</p>
|
||||
<ul class="plan-features">
|
||||
<li><i class="ph ph-check-circle"></i>5 sections</li>
|
||||
<li><i class="ph ph-check-circle"></i>Mobile responsive</li>
|
||||
<li><i class="ph ph-check-circle"></i>AI content editor</li>
|
||||
<li><i class="ph ph-check-circle"></i>SEO ขั้นสูง</li>
|
||||
<li><i class="ph ph-check-circle"></i>1 เดือน support</li>
|
||||
<li><i class="ph ph-check-circle"></i>Hosting หรือ deploy ให้</li>
|
||||
</ul>
|
||||
<a href="/contact" class="btn btn-primary btn-lg">ติดต่อสอบถาม</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pricing-note">* ราคาไม่รวมโดเมนและโฮสติ้ง ราคาขึ้นอยู่กับความซับซ้อนของงาน</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section cta-dark">
|
||||
<div class="container cta-center">
|
||||
<h2>พร้อมสร้างเว็บไซต์แล้วหรือยัง?</h2>
|
||||
<p>ติดต่อเราวันนี้ พร้อมให้คำปรึกษาฟรี</p>
|
||||
<a href="/contact" class="btn btn-primary btn-lg">ปรึกษาฟรี</a>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.service-hero {
|
||||
background: var(--color-dark);
|
||||
color: #f5f5f5;
|
||||
padding: var(--spacing-5xl) 0;
|
||||
}
|
||||
|
||||
.service-hero .container { max-width: 720px; }
|
||||
|
||||
.service-badge {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
color: var(--accent, var(--color-primary));
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
border-radius: var(--radius-full);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.service-hero h1 { color: #f5f5f5; margin-bottom: var(--spacing-lg); }
|
||||
.service-hero .lead { font-size: var(--font-size-xl); color: #a3a3a3; margin-bottom: var(--spacing-xl); }
|
||||
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
margin-top: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.tech-card {
|
||||
background: var(--color-bg-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tech-card h3 { font-size: var(--font-size-xl); margin-bottom: var(--spacing-sm); }
|
||||
.tech-card p { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin: 0; }
|
||||
|
||||
.cta-dark { background: var(--color-dark); color: #f5f5f5; }
|
||||
.cta-center { text-align: center; max-width: 600px; margin: 0 auto; }
|
||||
.cta-dark h2 { color: #f5f5f5; margin-bottom: var(--spacing-md); }
|
||||
.cta-dark p { color: #a3a3a3; margin-bottom: var(--spacing-xl); }
|
||||
|
||||
/* Pricing section */
|
||||
.pricing-header { text-align: center; margin-bottom: var(--spacing-3xl); }
|
||||
.pricing-header h2 { font-size: var(--font-size-4xl); margin-bottom: var(--spacing-sm); }
|
||||
.pricing-sub { font-size: var(--font-size-lg); color: var(--color-text-secondary); margin: 0; }
|
||||
|
||||
.pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pricing-card {
|
||||
position: relative;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pricing-card-highlighted {
|
||||
border-color: var(--color-primary);
|
||||
border-width: 2px;
|
||||
background: var(--color-dark);
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
.pricing-badge {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-dark);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
border-radius: var(--radius-full);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.plan-name { font-size: var(--font-size-xl); font-weight: 600; margin-bottom: var(--spacing-md); }
|
||||
.pricing-card-highlighted .plan-name { color: var(--color-primary); }
|
||||
|
||||
.plan-price { display: flex; align-items: baseline; gap: var(--spacing-xs); margin-bottom: var(--spacing-md); }
|
||||
.price-amount { font-family: var(--font-display); font-size: var(--font-size-5xl); font-weight: 700; line-height: 1; }
|
||||
.price-period { font-size: var(--font-size-sm); color: var(--color-muted); }
|
||||
.pricing-card-highlighted .price-period { color: #a3a3a3; }
|
||||
|
||||
.plan-description { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin-bottom: var(--spacing-lg); }
|
||||
.pricing-card-highlighted .plan-description { color: #a3a3a3; }
|
||||
|
||||
.plan-features { list-style: none; padding: 0; margin: 0 0 var(--spacing-xl); flex-grow: 1; }
|
||||
.plan-features li {
|
||||
display: flex; align-items: flex-start; gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm); padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.pricing-card-highlighted .plan-features li { border-bottom-color: #262626; }
|
||||
.plan-features li:last-child { border-bottom: none; }
|
||||
.plan-features i { color: var(--color-success); font-size: var(--font-size-lg); flex-shrink: 0; margin-top: 2px; }
|
||||
|
||||
.pricing-card .btn { width: 100%; justify-content: center; }
|
||||
.pricing-card-highlighted .btn-primary { background: var(--color-primary); color: var(--color-dark); }
|
||||
|
||||
.pricing-note { text-align: center; font-size: var(--font-size-sm); color: var(--color-muted); margin-top: var(--spacing-xl); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user