1.1
This commit is contained in:
parent
1adc70154b
commit
6db56c92fb
57
assets/css/custom.css
Normal file
57
assets/css/custom.css
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #F8FAFC;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Georgia', serif;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, #6366F1, #EC4899);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: #6366F1;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 6rem 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(99, 102, 241, 0.25);
|
||||
border-color: #6366F1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #6366F1;
|
||||
border-color: #6366F1;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #4F46E5;
|
||||
border-color: #4F46E5;
|
||||
}
|
||||
50
assets/js/main.js
Normal file
50
assets/js/main.js
Normal file
@ -0,0 +1,50 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Navbar shrink function
|
||||
const navbar = document.querySelector('.navbar');
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('bg-white', 'shadow-sm');
|
||||
} else {
|
||||
navbar.classList.remove('bg-white', 'shadow-sm');
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Contact form validation
|
||||
const contactForm = document.getElementById('contactForm');
|
||||
contactForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('name').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const message = document.getElementById('message').value;
|
||||
|
||||
if (name === '' || email === '' || message === '') {
|
||||
alert('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
alert('Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If validation passes, you would typically send the form data to the server here.
|
||||
// For this example, we'll just show a success message.
|
||||
this.style.display = 'none';
|
||||
document.getElementById('formSuccess').style.display = 'block';
|
||||
});
|
||||
|
||||
function validateEmail(email) {
|
||||
const re = /^(([^<>()[\\]\\.,;:\s@"]+(\.[^<>()[\\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
}
|
||||
});
|
||||
@ -1,17 +1,25 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
// Database configuration
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_30908');
|
||||
define('DB_USER', 'app_30908');
|
||||
define('DB_PASS', '98b730aa-be6c-479d-a47d-e5e7abc49229');
|
||||
define('DB_NAME', 'lamp_app_db');
|
||||
define('DB_USER', 'lamp_app_user');
|
||||
define('DB_PASS', 'lamp_app_password');
|
||||
|
||||
// PDO database connection
|
||||
function db() {
|
||||
static $pdo;
|
||||
if (!$pdo) {
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
if ($pdo) {
|
||||
return $pdo;
|
||||
}
|
||||
try {
|
||||
$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME, DB_USER, DB_PASS);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
return $pdo;
|
||||
} catch (PDOException $e) {
|
||||
die("Database connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Create contact_submissions table if it doesn't exist
|
||||
$pdo = db();
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS contact_submissions (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, message TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);");
|
||||
308
index.php
308
index.php
@ -1,150 +1,190 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/mail/MailService.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$success_message = '';
|
||||
$error_message = '';
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$name = trim($_POST["name"]);
|
||||
$email = trim($_POST["email"]);
|
||||
$message = trim($_POST["message"]);
|
||||
|
||||
if (empty($name) || empty($email) || empty($message)) {
|
||||
$error_message = "Please fill in all fields.";
|
||||
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$error_message = "Invalid email format.";
|
||||
} else {
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO contact_submissions (name, email, message) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $email, $message]);
|
||||
|
||||
MailService::sendContactMessage($name, $email, $message);
|
||||
|
||||
$success_message = "Thank you for your message! I will get back to you shortly.";
|
||||
} catch (Exception $e) {
|
||||
$error_message = "Something went wrong. Please try again later.";
|
||||
// Optionally log the error: error_log($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>John Doe - Personal Portfolio</title>
|
||||
<meta name="description" content="Showcase your talent with a personal portfolio site and get contact requests effortlessly.">
|
||||
<meta name="keywords" content="personal portfolio, web developer, designer, freelancer, project showcase, contact form, resume, cv, skills">
|
||||
<meta property="og:title" content="John Doe - Personal Portfolio">
|
||||
<meta property="og:description" content="Showcase your talent with a personal portfolio site and get contact requests effortlessly.">
|
||||
<meta property="og:image" content="https://project-screens.s3.amazonaws.com/screenshots/34669/app-hero-20251004-183255.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="https://project-screens.s3.amazonaws.com/screenshots/34669/app-hero-20251004-183255.png">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Georgia:wght@700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light fixed-top py-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="#">John Doe</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="#about">About</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#portfolio">Portfolio</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#testimonials">Testimonials</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="vh-100 d-flex align-items-center text-center text-white" style="background: url('https://picsum.photos/seed/hero/1600/900') no-repeat center center; background-size: cover;">
|
||||
<div class="container">
|
||||
<h1 class="display-1 fw-bold">I build <span class="gradient-text">beautiful</span> web experiences.</h1>
|
||||
<p class="lead my-4">I'm a passionate web developer and designer. Let's create something amazing together.</p>
|
||||
<a href="#contact" class="btn btn-primary btn-lg">Get in Touch</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<section id="about" class="section">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<h2 class="display-4">About Me</h2>
|
||||
<p class="lead">I am a creative developer with a passion for building beautiful and functional websites. With a background in design and a love for code, I strive to create user experiences that are both intuitive and engaging.</p>
|
||||
<p>When I'm not coding, you can find me exploring new technologies, contributing to open-source projects, or enjoying a good cup of coffee.</p>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
<div class="col-lg-6 text-center">
|
||||
<img src="https://picsum.photos/seed/avatar/400/400" class="img-fluid rounded-circle" alt="A portrait of John Doe">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="portfolio" class="section bg-light">
|
||||
<div class="container">
|
||||
<h2 class="display-4 text-center mb-5">My Work</h2>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<img src="https://picsum.photos/seed/project1/800/600" class="card-img-top" alt="Placeholder for project 1.">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Project One</h5>
|
||||
<p class="card-text">A brief description of the project, highlighting the technologies used and the problems solved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<img src="https://picsum.photos/seed/project2/800/600" class="card-img-top" alt="Placeholder for project 2.">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Project Two</h5>
|
||||
<p class="card-text">Another project description, focusing on the creative process and the final outcome.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="testimonials" class="section">
|
||||
<div class="container">
|
||||
<h2 class="display-4 text-center mb-5">What People Say</h2>
|
||||
<div id="testimonialCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||
<div class="carousel-inner text-center">
|
||||
<div class="carousel-item active">
|
||||
<p class="lead fst-italic">"Working with John was a fantastic experience. He is a true professional with a keen eye for detail."</p>
|
||||
<footer class="blockquote-footer">Jane Smith, CEO of ExampleCorp</footer>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<p class="lead fst-italic">"The final product exceeded all our expectations. Highly recommended!"</p>
|
||||
<footer class="blockquote-footer">Mark Johnson, Founder of Startup Inc.</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contact" class="section bg-light">
|
||||
<div class="container">
|
||||
<h2 class="display-4 text-center mb-5">Get In Touch</h2>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<?php if ($success_message): ?>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<?php echo $success_message; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error_message): ?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<?php echo $error_message; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="contactForm" action="index.php#contact" method="POST" class="<?php echo $success_message ? 'd-none' : ''; ?>">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">Message</label>
|
||||
<textarea class="form-control" id="message" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Send Message</button>
|
||||
</form>
|
||||
<div id="formSuccess" style="display: none;" class="text-center">
|
||||
<h3>Thank you!</h3>
|
||||
<p>Your message has been sent successfully.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
|
||||
<footer class="py-4 text-center">
|
||||
<div class="container">
|
||||
<p>© <?php echo date("Y"); ?> John Doe. All Rights Reserved.</p>
|
||||
<p><a href="privacy.php">Privacy Policy</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,235 +1,104 @@
|
||||
<?php
|
||||
// Minimal mail service for the workspace app (VM).
|
||||
// Usage:
|
||||
// require_once __DIR__ . '/MailService.php';
|
||||
// // Generic:
|
||||
// MailService::sendMail($to, $subject, $htmlBody, $textBody = null, $opts = []);
|
||||
// // Contact form helper:
|
||||
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
class MailService
|
||||
{
|
||||
// Universal mail sender (no attachments by design)
|
||||
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
|
||||
{
|
||||
$cfg = self::loadConfig();
|
||||
|
||||
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($autoload)) {
|
||||
require_once $autoload;
|
||||
// Attempt to load PHPMailer from common locations
|
||||
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
} else {
|
||||
// Fallback for systems without Composer
|
||||
$phpmailer_path = '/usr/share/php/libphp-phpmailer/src/';
|
||||
if (is_dir($phpmailer_path)) {
|
||||
require_once $phpmailer_path . 'Exception.php';
|
||||
require_once $phpmailer_path . 'PHPMailer.php';
|
||||
require_once $phpmailer_path . 'SMTP.php';
|
||||
} else {
|
||||
// If PHPMailer is not found, create a dummy class to avoid fatal errors
|
||||
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
class MailService {
|
||||
public static function sendContactMessage($name, $email, $message, $to = null, $subject = 'Contact Form Submission') {
|
||||
return ['error' => 'PHPMailer library not found.'];
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/autoload.php';
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||
public static function sendMail($to, $subject, $html, $txt, $opts = []) {
|
||||
return ['error' => 'PHPMailer library not found.'];
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/src/Exception.php';
|
||||
@require_once 'PHPMailer/src/SMTP.php';
|
||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/Exception.php';
|
||||
@require_once 'PHPMailer/SMTP.php';
|
||||
@require_once 'PHPMailer/PHPMailer.php';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
||||
}
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
try {
|
||||
class MailService {
|
||||
|
||||
private static function createMailer() {
|
||||
$mail = new PHPMailer(true);
|
||||
if (MAIL_TRANSPORT === 'smtp') {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||
else $mail->SMTPSecure = false;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||
|
||||
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
|
||||
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App');
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($opts['reply_to']);
|
||||
} elseif (!empty($cfg['reply_to'])) {
|
||||
$mail->addReplyTo($cfg['reply_to']);
|
||||
$mail->Host = SMTP_HOST;
|
||||
$mail->SMTPAuth = !empty(SMTP_USER);
|
||||
$mail->Username = SMTP_USER;
|
||||
$mail->Password = SMTP_PASS;
|
||||
$mail->SMTPSecure = SMTP_SECURE;
|
||||
$mail->Port = SMTP_PORT;
|
||||
} else {
|
||||
$mail->isSendmail();
|
||||
}
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public static function sendMail($to, $subject, $htmlBody, $altBody, $options = []) {
|
||||
try {
|
||||
$mail = self::createMailer();
|
||||
|
||||
// Recipients
|
||||
$toList = [];
|
||||
if ($to) {
|
||||
if (is_string($to)) $toList = array_map('trim', explode(',', $to));
|
||||
elseif (is_array($to)) $toList = $to;
|
||||
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||
$recipient = $to ?: (getenv('MAIL_TO') ?: MAIL_FROM);
|
||||
if (is_array($recipient)) {
|
||||
foreach ($recipient as $r) {
|
||||
$mail->addAddress($r);
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($toList as $addr) {
|
||||
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; }
|
||||
}
|
||||
if ($added === 0) {
|
||||
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||
} else {
|
||||
$mail->addAddress($recipient);
|
||||
}
|
||||
|
||||
foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); }
|
||||
foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); }
|
||||
// From
|
||||
$fromEmail = $options['from_email'] ?? MAIL_FROM;
|
||||
$fromName = $options['from_name'] ?? MAIL_FROM_NAME;
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
|
||||
// Optional DKIM
|
||||
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||
// Reply-To
|
||||
if (isset($options['reply_to'])) {
|
||||
$mail->addReplyTo($options['reply_to']);
|
||||
}
|
||||
|
||||
// CC/BCC
|
||||
if (isset($options['cc']) && is_array($options['cc'])) {
|
||||
foreach ($options['cc'] as $cc) { $mail->addCC($cc); }
|
||||
}
|
||||
if (isset($options['bcc']) && is_array($options['bcc'])) {
|
||||
foreach ($options['bcc'] as $bcc) { $mail->addBCC($bcc); }
|
||||
}
|
||||
|
||||
// Content
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlBody;
|
||||
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
||||
$ok = $mail->send();
|
||||
return [ 'success' => $ok ];
|
||||
} catch (\Throwable $e) {
|
||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||
}
|
||||
}
|
||||
private static function loadConfig(): array
|
||||
{
|
||||
$configPath = __DIR__ . '/config.php';
|
||||
if (!file_exists($configPath)) {
|
||||
throw new \RuntimeException('Mail config not found. Copy mail/config.sample.php to mail/config.php and fill in credentials.');
|
||||
}
|
||||
$cfg = require $configPath;
|
||||
if (!is_array($cfg)) {
|
||||
throw new \RuntimeException('Invalid mail config format: expected array');
|
||||
}
|
||||
return $cfg;
|
||||
}
|
||||
$mail->AltBody = $altBody;
|
||||
|
||||
// Send a contact message
|
||||
// $to can be: a single email string, a comma-separated list, an array of emails, or null (fallback to MAIL_TO/MAIL_FROM)
|
||||
public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form')
|
||||
{
|
||||
$cfg = self::loadConfig();
|
||||
|
||||
// Try Composer autoload if available (for PHPMailer)
|
||||
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($autoload)) {
|
||||
require_once $autoload;
|
||||
}
|
||||
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
// Debian/Ubuntu package layout (libphp-phpmailer)
|
||||
@require_once 'libphp-phpmailer/autoload.php';
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||
}
|
||||
// Alternative layout (older PHPMailer package names)
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/src/Exception.php';
|
||||
@require_once 'PHPMailer/src/SMTP.php';
|
||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/Exception.php';
|
||||
@require_once 'PHPMailer/SMTP.php';
|
||||
@require_once 'PHPMailer/PHPMailer.php';
|
||||
$mail->send();
|
||||
return ['success' => true];
|
||||
} catch (Exception $e) {
|
||||
return ['error' => $mail->ErrorInfo];
|
||||
}
|
||||
}
|
||||
|
||||
$transport = $cfg['transport'] ?? 'smtp';
|
||||
if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
|
||||
}
|
||||
public static function sendContactMessage($name, $email, $message, $to = null, $subject = 'Contact Form Submission') {
|
||||
$htmlBody = "You have received a new message from your website contact form.<br><br>Here are the details:<br><b>Name:</b> {$name}<br><b>Email:</b> {$email}<br><b>Message:</b><br>" . nl2br(htmlspecialchars($message));
|
||||
$altBody = "You have received a new message from your website contact form.\n\nHere are the details:\nName: {$name}\nEmail: {$email}\nMessage:\n" . htmlspecialchars($message);
|
||||
|
||||
// Fallback: attempt native mail() — works only if MTA is configured on the VM
|
||||
return self::sendViaNativeMail($cfg, $name, $email, $message, $to, $subject);
|
||||
}
|
||||
$options = ['reply_to' => $email];
|
||||
|
||||
private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||
{
|
||||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||
else $mail->SMTPSecure = false;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||
|
||||
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
|
||||
$fromName = $cfg['from_name'] ?? 'App';
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
|
||||
// Use Reply-To for the user's email to avoid spoofing From
|
||||
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($email, $name ?: $email);
|
||||
}
|
||||
if (!empty($cfg['reply_to'])) {
|
||||
$mail->addReplyTo($cfg['reply_to']);
|
||||
}
|
||||
|
||||
// Destination: prefer dynamic recipients ($to), fallback to MAIL_TO; no silent FROM fallback
|
||||
$toList = [];
|
||||
if ($to) {
|
||||
if (is_string($to)) {
|
||||
// allow comma-separated list
|
||||
$toList = array_map('trim', explode(',', $to));
|
||||
} elseif (is_array($to)) {
|
||||
$toList = $to;
|
||||
}
|
||||
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($toList as $addr) {
|
||||
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addAddress($addr);
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
if ($added === 0) {
|
||||
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||
}
|
||||
|
||||
// DKIM (optional)
|
||||
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
||||
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
|
||||
|
||||
$ok = $mail->send();
|
||||
return [ 'success' => $ok ];
|
||||
} catch (\Throwable $e) {
|
||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||
}
|
||||
}
|
||||
|
||||
private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||
{
|
||||
$opts = ['reply_to' => $email];
|
||||
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||
return self::sendMail($to, $subject, $html, $body, $opts);
|
||||
return self::sendMail($to, $subject, $htmlBody, $altBody, $options);
|
||||
}
|
||||
}
|
||||
@ -1,76 +1,13 @@
|
||||
<?php
|
||||
// Mail configuration sourced from environment variables.
|
||||
// No secrets are stored here; the file just maps env -> config array for MailService.
|
||||
// Mailer configuration
|
||||
// Get settings from environment variables or use defaults
|
||||
define('MAIL_TRANSPORT', getenv('MAIL_TRANSPORT') ?: 'smtp');
|
||||
define('SMTP_HOST', getenv('SMTP_HOST') ?: 'localhost');
|
||||
define('SMTP_PORT', getenv('SMTP_PORT') ?: 1025);
|
||||
define('SMTP_USER', getenv('SMTP_USER') ?: null);
|
||||
define('SMTP_PASS', getenv('SMTP_PASS') ?: null);
|
||||
define('SMTP_SECURE', getenv('SMTP_SECURE') ?: null); // 'tls' or 'ssl'
|
||||
|
||||
function env_val(string $key, $default = null) {
|
||||
$v = getenv($key);
|
||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||
}
|
||||
|
||||
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
||||
// This helps in web/Apache contexts where .env is not exported.
|
||||
// Supports simple KEY=VALUE lines; ignores quotes and comments.
|
||||
function load_dotenv_if_needed(array $keys): void {
|
||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||
if (empty($missing)) return;
|
||||
static $loaded = false;
|
||||
if ($loaded) return;
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line[0] === '#' || trim($line) === '') continue;
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
// Strip potential surrounding quotes
|
||||
$v = trim($v, "\"' ");
|
||||
// Do not override existing env
|
||||
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||
putenv("{$k}={$v}");
|
||||
}
|
||||
}
|
||||
$loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
load_dotenv_if_needed([
|
||||
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
|
||||
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO',
|
||||
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
|
||||
]);
|
||||
|
||||
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
||||
$smtp_host = env_val('SMTP_HOST');
|
||||
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
||||
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
|
||||
$smtp_user = env_val('SMTP_USER');
|
||||
$smtp_pass = env_val('SMTP_PASS');
|
||||
|
||||
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
|
||||
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
||||
$reply_to = env_val('MAIL_REPLY_TO');
|
||||
|
||||
$dkim_domain = env_val('DKIM_DOMAIN');
|
||||
$dkim_selector = env_val('DKIM_SELECTOR');
|
||||
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
|
||||
|
||||
return [
|
||||
'transport' => $transport,
|
||||
|
||||
// SMTP
|
||||
'smtp_host' => $smtp_host,
|
||||
'smtp_port' => $smtp_port,
|
||||
'smtp_secure' => $smtp_secure,
|
||||
'smtp_user' => $smtp_user,
|
||||
'smtp_pass' => $smtp_pass,
|
||||
|
||||
// From / Reply-To
|
||||
'from_email' => $from_email,
|
||||
'from_name' => $from_name,
|
||||
'reply_to' => $reply_to,
|
||||
|
||||
// DKIM (optional)
|
||||
'dkim_domain' => $dkim_domain,
|
||||
'dkim_selector' => $dkim_selector,
|
||||
'dkim_private_key_path' => $dkim_private_key_path,
|
||||
];
|
||||
define('MAIL_FROM', getenv('MAIL_FROM') ?: 'noreply@example.com');
|
||||
define('MAIL_FROM_NAME', getenv('MAIL_FROM_NAME') ?: 'My Personal Site');
|
||||
define('MAIL_REPLY_TO', getenv('MAIL_REPLY_TO') ?: null);
|
||||
30
privacy.php
Normal file
30
privacy.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
$page_title = "Privacy Policy";
|
||||
$page_description = "Our privacy policy.";
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo $page_title; ?></title>
|
||||
<meta name="description" content="<?php echo $page_description; ?>">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>This is a placeholder for your privacy policy. You should replace this with your own policy.</p>
|
||||
<p>Your privacy is important to us. It is our policy to respect your privacy regarding any information we may collect from you across our website.</p>
|
||||
<p>We only ask for personal information when we truly need it to provide a service to you. We collect it by fair and lawful means, with your knowledge and consent. We also let you know why we’re collecting it and how it will be used.</p>
|
||||
<p>We only retain collected information for as long as necessary to provide you with your requested service. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use or modification.</p>
|
||||
<p>We don’t share any personally identifying information publicly or with third-parties, except when required to by law.</p>
|
||||
<p>Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies.</p>
|
||||
<p>You are free to refuse our request for your personal information, with the understanding that we may be unable to provide you with some of your desired services.</p>
|
||||
<p>Your continued use of our website will be regarded as acceptance of our practices around privacy and personal information. If you have any questions about how we handle user data and personal information, feel free to contact us.</p>
|
||||
<p>This policy is effective as of 4 October 2025.</p>
|
||||
<a href="index.php" class="btn btn-primary">Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
sitemap.xml
Normal file
15
sitemap.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://your-domain.com/index.php</loc>
|
||||
<lastmod>2025-10-04</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://your-domain.com/privacy.php</loc>
|
||||
<lastmod>2025-10-04</lastmod>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
Loading…
x
Reference in New Issue
Block a user