Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
aeb101f333 5-29-26-Initial 2026-05-29 06:25:15 +00:00
9 changed files with 723 additions and 572 deletions

View File

@ -1,403 +1,150 @@
:root {
--color-bg: #f7f7f5;
--color-surface: #ffffff;
--color-surface-2: #eeeeeb;
--color-text: #181817;
--color-muted: #686865;
--color-border: #d9d9d3;
--color-accent: #111111;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--shadow-sm: 0 1px 2px rgba(20, 20, 20, .05);
--shadow-md: 0 12px 30px rgba(20, 20, 20, .08);
--space-section: clamp(3rem, 7vw, 6rem);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 15px;
line-height: 1.55;
}
.main-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
a { color: inherit; text-underline-offset: 3px; }
a:hover { color: #000; }
.site-header {
background: rgba(247, 247, 245, .92);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--color-border);
}
.navbar { padding-block: .75rem; }
.navbar-brand { font-weight: 700; letter-spacing: -.03em; }
.nav-link { color: var(--color-muted); font-weight: 600; font-size: .92rem; }
.nav-link.active, .nav-link:hover { color: var(--color-text); }
.navbar-toggler { border-radius: var(--radius-sm); border-color: var(--color-border); }
.btn {
border-radius: var(--radius-sm);
font-weight: 700;
letter-spacing: -.01em;
padding: .72rem 1rem;
}
.btn-lg { padding: .86rem 1.15rem; font-size: .98rem; }
.btn-dark { background: var(--color-accent); border-color: var(--color-accent); }
.btn-outline-dark { border-color: #2c2c2a; }
.btn:focus-visible, .form-control:focus, .form-select:focus {
box-shadow: 0 0 0 .22rem rgba(24, 24, 23, .14);
border-color: var(--color-text);
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
.hero-section { padding: clamp(3rem, 8vw, 6.5rem) 0 var(--space-section); }
.eyebrow {
color: var(--color-muted);
font-size: .76rem;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
h1, h2, h3, .page-title {
color: var(--color-text);
letter-spacing: -.045em;
line-height: 1.05;
}
h1 { font-size: clamp(2.35rem, 6vw, 4.75rem); font-weight: 800; max-width: 11ch; }
h2 { font-size: clamp(1.65rem, 3.5vw, 2.75rem); font-weight: 800; }
h3 { font-size: 1.05rem; font-weight: 800; }
.page-title { font-size: clamp(2rem, 4vw, 3.25rem); font-weight: 800; }
.lead-copy { color: var(--color-muted); font-size: clamp(1rem, 2vw, 1.14rem); max-width: 42rem; }
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
.metric-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: .75rem;
max-width: 34rem;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
.metric-row div, .feature-card, .panel, .form-shell, .confirmation-card, .table-card, .detail-card, .empty-state {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.metric-row div { padding: .9rem; }
.metric-row dt { font-size: 1.1rem; font-weight: 800; line-height: 1; }
.metric-row dd { margin: .25rem 0 0; color: var(--color-muted); font-size: .82rem; }
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
.panel { padding: 1rem; }
.hero-panel { max-width: 34rem; margin-left: auto; }
.panel-header {
display: flex; align-items: center; gap: .5rem;
border-bottom: 1px solid var(--color-border);
padding-bottom: .85rem; margin-bottom: 1rem;
color: var(--color-muted); font-size: .86rem; font-weight: 700;
}
.status-dot { width: .55rem; height: .55rem; border-radius: 50%; background: #198754; display: inline-block; }
.preview-card { background: var(--color-surface-2); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 1rem; margin-bottom: 1rem; }
.preview-line { height: .7rem; background: #cfcfca; border-radius: 999px; margin-bottom: .65rem; }
.preview-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem; margin-top: 1rem; }
.preview-grid span { min-height: 4.7rem; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-sm); }
.check-list { list-style: none; padding: 0; display: grid; gap: .55rem; color: var(--color-muted); }
.check-list li { position: relative; padding-left: 1.35rem; }
.check-list li::before { content: "✓"; position: absolute; left: 0; color: var(--color-text); font-weight: 900; }
::-webkit-scrollbar-track {
background: transparent;
.section { padding: var(--space-section) 0; }
.muted-section { background: var(--color-surface-2); border-block: 1px solid var(--color-border); }
.section-heading { max-width: 44rem; margin-bottom: 1.5rem; }
.section-heading p:not(.eyebrow) { color: var(--color-muted); }
.feature-card { padding: 1.25rem; height: 100%; }
.feature-card p { color: var(--color-muted); margin-bottom: 0; }
.timeline-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
.timeline-list li { display: grid; grid-template-columns: 3rem 1fr; gap: 1rem; padding: 1rem; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
.timeline-list span { font-weight: 900; color: var(--color-muted); }
.timeline-list p { margin: .2rem 0 0; color: var(--color-muted); }
.form-shell, .confirmation-card, .detail-card, .empty-state { padding: clamp(1.2rem, 3vw, 2rem); }
.form-label { font-weight: 700; font-size: .9rem; }
.form-control, .form-select { border-radius: var(--radius-sm); border-color: var(--color-border); padding: .78rem .86rem; }
.hp-field { position: absolute; left: -10000px; width: 1px; height: 1px; opacity: 0; }
.invalid-feedback { font-size: .8rem; }
.alert { border-radius: var(--radius-sm); border: 1px solid var(--color-border); }
.confirmation-page { min-height: 64vh; display: flex; align-items: center; }
.submitted-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; }
.submitted-summary div { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: .9rem; background: var(--color-bg); }
.submitted-summary span { display: block; color: var(--color-muted); font-size: .78rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
.submitted-summary strong { display: block; margin-top: .18rem; overflow-wrap: anywhere; }
.table-card { overflow: hidden; }
.table { --bs-table-bg: transparent; }
.table th { color: var(--color-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; }
.table td, .table th { padding: 1rem; border-color: var(--color-border); }
.empty-state { text-align: center; padding-block: 4rem; }
.empty-state p { color: var(--color-muted); }
.back-link { color: var(--color-muted); font-weight: 700; text-decoration: none; }
.back-link:hover { color: var(--color-text); }
.detail-layout { max-width: 940px; }
.message-box { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 1rem; white-space: normal; }
.site-footer { padding: 1.5rem 0; border-top: 1px solid var(--color-border); color: var(--color-muted); font-size: .88rem; }
@media (max-width: 767.98px) {
h1 { max-width: none; }
.metric-row, .submitted-summary { grid-template-columns: 1fr; }
.hero-panel { margin-left: 0; }
.table td, .table th { white-space: nowrap; }
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.header-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.header-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-links {
display: flex;
gap: 1rem;
}
.admin-card {
background: rgba(255, 255, 255, 0.6);
padding: 2rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
margin-bottom: 2.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.admin-card h3 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 700;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
.btn-save {
background: #0088cc;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
width: 100%;
transition: all 0.3s ease;
}
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
}
.history-table-container {
overflow-x: auto;
background: rgba(255, 255, 255, 0.4);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.history-table {
width: 100%;
}
.history-table-time {
width: 15%;
white-space: nowrap;
font-size: 0.85em;
color: #555;
}
.history-table-user {
width: 35%;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px;
}
.history-table-ai {
width: 50%;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 8px;
}
.no-messages {
text-align: center;
color: #777;
}

View File

@ -1,39 +1,26 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
}
(() => {
const forms = document.querySelectorAll('.needs-validation');
forms.forEach((form) => {
form.addEventListener('submit', (event) => {
const message = form.querySelector('textarea[name="message"]');
if (message && message.value.trim().length < 10) {
message.setCustomValidity('Please enter at least 10 characters.');
} else if (message) {
message.setCustomValidity('');
}
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
});
});
});
const textarea = document.querySelector('textarea[name="message"]');
const counter = document.querySelector('[data-char-count]');
if (textarea && counter) {
const update = () => { counter.textContent = String(textarea.value.length); };
textarea.addEventListener('input', update);
update();
}
})();

78
contact.php Normal file
View File

@ -0,0 +1,78 @@
<?php
require_once __DIR__ . '/includes/app.php';
require_once __DIR__ . '/mail/MailService.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: index.php#lead-form');
exit;
}
function back_with_error(string $message): never
{
header('Location: index.php?error=' . urlencode($message) . '#lead-form');
exit;
}
$name = trim((string)($_POST['name'] ?? ''));
$email = trim((string)($_POST['email'] ?? ''));
$company = trim((string)($_POST['company'] ?? ''));
$budget = trim((string)($_POST['budget'] ?? ''));
$message = trim((string)($_POST['message'] ?? ''));
$honeypot = trim((string)($_POST['website'] ?? ''));
if ($honeypot !== '') {
header('Location: thank-you.php');
exit;
}
if ($name === '' || strlen($name) > 120) {
back_with_error('Name is required and must be under 120 characters.');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 190) {
back_with_error('A valid email address is required.');
}
if (strlen($message) < 10 || strlen($message) > 2000) {
back_with_error('Message must be between 10 and 2000 characters.');
}
if (strlen($company) > 160 || strlen($budget) > 80) {
back_with_error('One of the optional fields is too long.');
}
try {
ensure_leads_table();
$token = bin2hex(random_bytes(16));
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
$agent = substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255);
$stmt = db()->prepare('INSERT INTO leads (public_token, name, email, company, budget, message, source, ip_address, user_agent) VALUES (:token, :name, :email, :company, :budget, :message, :source, :ip, :agent)');
$stmt->bindValue(':token', $token);
$stmt->bindValue(':name', $name);
$stmt->bindValue(':email', $email);
$stmt->bindValue(':company', $company !== '' ? $company : null);
$stmt->bindValue(':budget', $budget !== '' ? $budget : null);
$stmt->bindValue(':message', $message);
$stmt->bindValue(':source', 'Landing page');
$stmt->bindValue(':ip', $ip);
$stmt->bindValue(':agent', $agent);
$stmt->execute();
$leadId = (int)db()->lastInsertId();
$safeName = e($name);
$safeEmail = e($email);
$safeMessage = nl2br(e($message));
$html = "<h2>New landing page lead</h2><p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><p><strong>Company:</strong> " . e($company ?: 'Not provided') . "</p><p><strong>Budget:</strong> " . e($budget ?: 'Not sure') . "</p><p><strong>Message:</strong><br>{$safeMessage}</p>";
$text = "New landing page lead\nName: {$name}\nEmail: {$email}\nCompany: " . ($company ?: 'Not provided') . "\nBudget: " . ($budget ?: 'Not sure') . "\n\n{$message}";
$mailResult = MailService::sendMail(null, 'New landing page lead from ' . $name, $html, $text, ['reply_to' => $email]);
if (!empty($mailResult['success'])) {
$update = db()->prepare('UPDATE leads SET email_sent = 1 WHERE id = :id');
$update->bindValue(':id', $leadId, PDO::PARAM_INT);
$update->execute();
} else {
error_log('Lead notification email failed: ' . ($mailResult['error'] ?? 'unknown error'));
}
header('Location: thank-you.php?token=' . urlencode($token));
exit;
} catch (Throwable $exception) {
error_log('Lead submission failed: ' . $exception->getMessage());
back_with_error('We could not save your request right now. Please try again in a moment.');
}

9
healthz/index.php Normal file
View File

@ -0,0 +1,9 @@
<?php
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
'ok' => true,
'service' => 'landing-leads',
'time' => gmdate('c'),
'php' => PHP_VERSION,
], JSON_UNESCAPED_SLASHES);

167
includes/app.php Normal file
View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
function e(?string $value): string
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function project_name(): string
{
$name = $_SERVER['PROJECT_NAME'] ?? $_SERVER['APP_NAME'] ?? getenv('PROJECT_NAME') ?: '';
return $name !== '' ? (string)$name : 'LaunchPage';
}
function short_text(?string $value, int $limit = 54): string
{
$text = trim((string)$value);
if (strlen($text) <= $limit) {
return $text;
}
return rtrim(substr($text, 0, max(0, $limit - 3))) . '…';
}
function project_description(): string
{
$description = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: '';
return $description !== '' ? $description : 'A focused landing page with a fast, secure lead capture workflow.';
}
function ensure_leads_table(): void
{
static $ready = false;
if ($ready) {
return;
}
require_once __DIR__ . '/../db/config.php';
$sql = "CREATE TABLE IF NOT EXISTS leads (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
public_token CHAR(32) NOT NULL UNIQUE,
name VARCHAR(120) NOT NULL,
email VARCHAR(190) NOT NULL,
company VARCHAR(160) NULL,
budget VARCHAR(80) NULL,
message TEXT NOT NULL,
source VARCHAR(120) NULL,
status ENUM('new','contacted','closed') NOT NULL DEFAULT 'new',
email_sent TINYINT(1) NOT NULL DEFAULT 0,
ip_address VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
db()->exec($sql);
$ready = true;
}
function lead_count(): int
{
ensure_leads_table();
$stmt = db()->query('SELECT COUNT(*) AS total FROM leads');
return (int)($stmt->fetch()['total'] ?? 0);
}
function latest_leads(int $limit = 8): array
{
ensure_leads_table();
$stmt = db()->prepare('SELECT id, public_token, name, email, company, budget, message, status, email_sent, created_at FROM leads ORDER BY created_at DESC LIMIT :limit');
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
function fetch_lead_by_token(string $token): ?array
{
ensure_leads_table();
$stmt = db()->prepare('SELECT * FROM leads WHERE public_token = :token LIMIT 1');
$stmt->bindValue(':token', $token, PDO::PARAM_STR);
$stmt->execute();
$lead = $stmt->fetch();
return $lead ?: null;
}
function fetch_lead_by_id(int $id): ?array
{
ensure_leads_table();
$stmt = db()->prepare('SELECT * FROM leads WHERE id = :id LIMIT 1');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$lead = $stmt->fetch();
return $lead ?: null;
}
function page_head(string $title, string $description = ''): void
{
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? getenv('PROJECT_IMAGE_URL') ?: '';
$metaDescription = $description !== '' ? $description : ($projectDescription !== '' ? $projectDescription : project_description());
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($title) ?></title>
<meta name="description" content="<?= e($metaDescription) ?>">
<?php if ($projectDescription): ?>
<!-- Meta description from project environment -->
<meta property="og:description" content="<?= e($projectDescription) ?>">
<meta property="twitter:description" content="<?= e($projectDescription) ?>">
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Platform-managed preview image -->
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
<?php endif; ?>
<meta property="og:title" content="<?= e($title) ?>">
<meta name="twitter:card" content="summary_large_image">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=2026052901">
</head>
<body>
<?php
}
function page_nav(string $active = 'home'): void
{
?>
<header class="site-header sticky-top">
<nav class="navbar navbar-expand-lg" aria-label="Primary navigation">
<div class="container">
<a class="navbar-brand" href="index.php" aria-label="<?= e(project_name()) ?> home"><?= e(project_name()) ?></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><a class="nav-link <?= $active === 'home' ? 'active' : '' ?>" href="index.php#offer">Offer</a></li>
<li class="nav-item"><a class="nav-link <?= $active === 'process' ? 'active' : '' ?>" href="index.php#process">Process</a></li>
<li class="nav-item"><a class="nav-link <?= $active === 'leads' ? 'active' : '' ?>" href="leads.php">Leads</a></li>
<li class="nav-item"><a class="btn btn-dark btn-sm ms-lg-2" href="index.php#lead-form">Request info</a></li>
</ul>
</div>
</div>
</nav>
</header>
<?php
}
function page_footer(): void
{
$year = date('Y');
?>
<footer class="site-footer">
<div class="container d-flex flex-column flex-md-row justify-content-between gap-2">
<p class="mb-0">&copy; <?= e((string)$year) ?> <?= e(project_name()) ?>. Built for fast lead capture.</p>
<p class="mb-0"><a href="leads.php">View leads</a> <span aria-hidden="true">·</span> <a href="/healthz">Health</a></p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=2026052901" defer></script>
</body>
</html>
<?php
}

297
index.php
View File

@ -1,150 +1,157 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$sent = isset($_GET['sent']) && $_GET['sent'] === '1';
$error = isset($_GET['error']) ? (string)$_GET['error'] : '';
$leadTotal = 0;
try {
$leadTotal = lead_count();
} catch (Throwable $exception) {
error_log('Lead count unavailable: ' . $exception->getMessage());
}
page_head(project_name() . ' — Landing Page & Lead Capture', project_description());
page_nav('home');
?>
<!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; ?>
<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>
</head>
<body>
<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>
<main>
<section class="hero-section" id="top">
<div class="container">
<?php if ($sent): ?>
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">
<strong>Request received.</strong> Thanks your details were saved and the team can follow up shortly.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php elseif ($error): ?>
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
<strong>Please review the form.</strong> <?= e($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="row align-items-center g-4 g-lg-5">
<div class="col-lg-6">
<p class="eyebrow mb-3">Professional landing page hosting</p>
<h1>Publish your offer and capture qualified leads in one focused page.</h1>
<p class="lead-copy">A restrained, conversion-ready landing shell with a secure contact workflow, confirmation page, and lead review area ready to replace with your uploaded HTML whenever you provide it.</p>
<div class="hero-actions d-flex flex-column flex-sm-row gap-2 mt-4">
<a class="btn btn-dark btn-lg" href="#lead-form">Request a quote</a>
<a class="btn btn-outline-dark btn-lg" href="leads.php">Review leads</a>
</div>
<dl class="metric-row mt-4" aria-label="Landing page metrics">
<div><dt><?= e((string)$leadTotal) ?></dt><dd>stored leads</dd></div>
<div><dt>24h</dt><dd>fast response target</dd></div>
<div><dt>PDO</dt><dd>secure storage</dd></div>
</dl>
</div>
<div class="col-lg-6">
<aside class="panel hero-panel" aria-label="Lead capture preview">
<div class="panel-header">
<span class="status-dot" aria-hidden="true"></span>
<span>Lead workflow active</span>
</div>
<div class="preview-card">
<div class="preview-line w-75"></div>
<div class="preview-line w-50"></div>
<div class="preview-grid">
<span></span><span></span><span></span><span></span>
</div>
</div>
<ul class="check-list mb-0">
<li>Server-side validation</li>
<li>Spam honeypot protection</li>
<li>Database-backed lead list and detail view</li>
<li>Email notification attempted via configured mail service</li>
</ul>
</aside>
</div>
</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>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
</section>
<section class="section" id="offer">
<div class="container">
<div class="section-heading">
<p class="eyebrow">What is included</p>
<h2>A complete first slice, not just a placeholder.</h2>
</div>
<div class="row g-3">
<div class="col-md-4"><article class="feature-card"><h3>Landing message</h3><p>Clear positioning, compact proof points, and focused calls-to-action for visitors.</p></article></div>
<div class="col-md-4"><article class="feature-card"><h3>Lead form</h3><p>Name, email, company, budget, and message with accessible validation and smooth feedback.</p></article></div>
<div class="col-md-4"><article class="feature-card"><h3>Lead review</h3><p>Submissions are saved to MariaDB and available in a concise list and detail page.</p></article></div>
</div>
</div>
</section>
<section class="section muted-section" id="process">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<p class="eyebrow">Visitor journey</p>
<h2>From interest to follow-up in under a minute.</h2>
</div>
<div class="col-lg-7">
<ol class="timeline-list">
<li><span>01</span><div><strong>Understand the offer</strong><p>Visitors get a concise promise and proof points immediately.</p></div></li>
<li><span>02</span><div><strong>Submit details</strong><p>The form validates required fields and blocks common bot noise.</p></div></li>
<li><span>03</span><div><strong>Confirm and review</strong><p>The lead is stored, a notification is attempted, and the lead detail is available.</p></div></li>
</ol>
</div>
</div>
</div>
</section>
<section class="section" id="lead-form">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-9 col-xl-8">
<div class="form-shell">
<div class="section-heading text-center mb-4">
<p class="eyebrow">Contact / lead form</p>
<h2>Tell us what you want to launch.</h2>
<p>Submissions are saved securely. Email delivery depends on your configured SMTP or MAIL_TO environment settings.</p>
</div>
<form class="needs-validation" action="contact.php" method="post" novalidate data-lead-form>
<input type="text" name="website" class="hp-field" tabindex="-1" autocomplete="off" aria-hidden="true">
<div class="row g-3">
<div class="col-md-6">
<label for="name" class="form-label">Name</label>
<input id="name" name="name" type="text" class="form-control" maxlength="120" required autocomplete="name">
<div class="invalid-feedback">Please enter your name.</div>
</div>
<div class="col-md-6">
<label for="email" class="form-label">Email</label>
<input id="email" name="email" type="email" class="form-control" maxlength="190" required autocomplete="email">
<div class="invalid-feedback">Please enter a valid email.</div>
</div>
<div class="col-md-6">
<label for="company" class="form-label">Company <span class="text-muted">optional</span></label>
<input id="company" name="company" type="text" class="form-control" maxlength="160" autocomplete="organization">
</div>
<div class="col-md-6">
<label for="budget" class="form-label">Budget range <span class="text-muted">optional</span></label>
<select id="budget" name="budget" class="form-select">
<option value="">Not sure yet</option>
<option value="Under $1,000">Under $1,000</option>
<option value="$1,000$5,000">$1,000$5,000</option>
<option value="$5,000$10,000">$5,000$10,000</option>
<option value="$10,000+">$10,000+</option>
</select>
</div>
<div class="col-12">
<label for="message" class="form-label">What do you need?</label>
<textarea id="message" name="message" class="form-control" rows="5" maxlength="2000" required placeholder="Share the landing page goal, offer, audience, or timeline."></textarea>
<div class="d-flex justify-content-between mt-1"><div class="invalid-feedback d-block">Please enter at least 10 characters.</div><small class="text-muted"><span data-char-count>0</span>/2000</small></div>
</div>
</div>
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-3 mt-4">
<button class="btn btn-dark btn-lg" type="submit">Send request</button>
<p class="small text-muted mb-0">Testing notice: Flatlogic does not guarantee mail server usage. Configure your own SMTP in environment MAIL_/SMTP_ variables before production.</p>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</main>
<?php page_footer(); ?>

57
lead.php Normal file
View File

@ -0,0 +1,57 @@
<?php
require_once __DIR__ . '/includes/app.php';
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$lead = null;
$error = '';
try {
if ($id > 0) {
$lead = fetch_lead_by_id($id);
}
if (!$lead) {
$error = 'Lead not found.';
}
} catch (Throwable $exception) {
$error = 'Lead detail is temporarily unavailable.';
error_log('Lead detail failed: ' . $exception->getMessage());
}
page_head('Lead Detail — ' . project_name(), 'Detailed view of a captured landing page lead.');
page_nav('leads');
?>
<main class="section">
<div class="container">
<a class="back-link" href="leads.php"> Back to leads</a>
<?php if ($error): ?>
<div class="alert alert-warning mt-3" role="alert"><?= e($error) ?></div>
<?php else: ?>
<div class="detail-layout mt-3">
<article class="detail-card">
<div class="d-flex flex-column flex-md-row justify-content-between gap-3 mb-4">
<div>
<p class="eyebrow">Lead #<?= e((string)$lead['id']) ?></p>
<h1 class="page-title mb-1"><?= e($lead['name']) ?></h1>
<p class="text-muted mb-0"><a href="mailto:<?= e($lead['email']) ?>"><?= e($lead['email']) ?></a></p>
</div>
<div class="text-md-end">
<span class="badge text-bg-light border"><?= e(ucfirst($lead['status'])) ?></span>
<?= !empty($lead['email_sent']) ? '<span class="badge text-bg-success">Email sent</span>' : '<span class="badge text-bg-secondary">Email not sent</span>' ?>
</div>
</div>
<div class="submitted-summary mb-4">
<div><span>Company</span><strong><?= e($lead['company'] ?: '—') ?></strong></div>
<div><span>Budget</span><strong><?= e($lead['budget'] ?: 'Not sure yet') ?></strong></div>
<div><span>Created</span><strong><?= e(date('M j, Y H:i', strtotime($lead['created_at']))) ?></strong></div>
</div>
<h2>Message</h2>
<p class="message-box"><?= nl2br(e($lead['message'])) ?></p>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-dark" href="mailto:<?= e($lead['email']) ?>?subject=Re:%20Your%20landing%20page%20request">Reply by email</a>
<a class="btn btn-outline-dark" href="index.php#lead-form">Capture another lead</a>
</div>
</article>
</div>
<?php endif; ?>
</div>
</main>
<?php page_footer(); ?>

60
leads.php Normal file
View File

@ -0,0 +1,60 @@
<?php
require_once __DIR__ . '/includes/app.php';
$leads = [];
$error = '';
try {
$leads = latest_leads(25);
} catch (Throwable $exception) {
$error = 'Leads are temporarily unavailable.';
error_log('Leads list failed: ' . $exception->getMessage());
}
page_head('Lead Inbox — ' . project_name(), 'Review captured landing page leads.');
page_nav('leads');
?>
<main class="section">
<div class="container">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end gap-3 mb-4">
<div>
<p class="eyebrow">Lead inbox</p>
<h1 class="page-title">Captured requests</h1>
<p class="text-muted mb-0">Newest submissions from the landing page form.</p>
</div>
<a class="btn btn-dark" href="index.php#lead-form">Add test lead</a>
</div>
<?php if ($error): ?>
<div class="alert alert-danger" role="alert"><?= e($error) ?></div>
<?php elseif (!$leads): ?>
<div class="empty-state">
<h2>No leads yet</h2>
<p>Submit the landing page form to see requests appear here.</p>
<a class="btn btn-dark" href="index.php#lead-form">Open form</a>
</div>
<?php else: ?>
<div class="table-card">
<div class="table-responsive">
<table class="table align-middle mb-0">
<caption class="visually-hidden">Captured landing page leads</caption>
<thead><tr><th>Name</th><th>Company</th><th>Budget</th><th>Status</th><th>Email</th><th>Created</th><th></th></tr></thead>
<tbody>
<?php foreach ($leads as $lead): ?>
<tr>
<td><strong><?= e($lead['name']) ?></strong><br><span class="text-muted small"><?= e(short_text($lead['message'], 54)) ?></span></td>
<td><?= e($lead['company'] ?: '—') ?></td>
<td><?= e($lead['budget'] ?: 'Not sure') ?></td>
<td><span class="badge text-bg-light border"><?= e(ucfirst($lead['status'])) ?></span></td>
<td><?= !empty($lead['email_sent']) ? '<span class="badge text-bg-success">Sent</span>' : '<span class="badge text-bg-secondary">Not sent</span>' ?></td>
<td><time datetime="<?= e($lead['created_at']) ?>"><?= e(date('M j, Y H:i', strtotime($lead['created_at']))) ?></time></td>
<td class="text-end"><a class="btn btn-outline-dark btn-sm" href="lead.php?id=<?= e((string)$lead['id']) ?>">View</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
</main>
<?php page_footer(); ?>

39
thank-you.php Normal file
View File

@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/includes/app.php';
$token = isset($_GET['token']) ? preg_replace('/[^a-f0-9]/', '', strtolower((string)$_GET['token'])) : '';
$lead = null;
if ($token !== '') {
try {
$lead = fetch_lead_by_token($token);
} catch (Throwable $exception) {
error_log('Thank-you lead lookup failed: ' . $exception->getMessage());
}
}
page_head('Thank You — ' . project_name(), 'Confirmation page for your landing page request.');
page_nav('home');
?>
<main class="section confirmation-page">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<article class="confirmation-card">
<p class="eyebrow">Request submitted</p>
<h1>Thank you<?= $lead ? ', ' . e($lead['name']) : '' ?>.</h1>
<p class="lead-copy">Your request has been saved. If email notifications are configured, the team also received a message with your details.</p>
<?php if ($lead): ?>
<div class="submitted-summary">
<div><span>Email</span><strong><?= e($lead['email']) ?></strong></div>
<div><span>Budget</span><strong><?= e($lead['budget'] ?: 'Not sure yet') ?></strong></div>
<div><span>Status</span><strong><?= e(ucfirst($lead['status'])) ?></strong></div>
</div>
<a class="btn btn-dark" href="lead.php?id=<?= e((string)$lead['id']) ?>">View saved lead</a>
<?php endif; ?>
<a class="btn btn-outline-dark" href="index.php">Back to landing page</a>
</article>
</div>
</div>
</div>
</main>
<?php page_footer(); ?>