Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e76255737 |
142
admin.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/leads.php';
|
||||
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Northline Studio';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Admin dashboard for reviewing agency quote requests.';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
$accessKey = getenv('ADMIN_ACCESS_KEY') ?: '';
|
||||
$demoMode = $accessKey === '';
|
||||
$loginError = '';
|
||||
|
||||
if (isset($_GET['logout'])) {
|
||||
unset($_SESSION['admin_ok']);
|
||||
header('Location: admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$demoMode && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$submitted = (string)($_POST['access_key'] ?? '');
|
||||
if (hash_equals($accessKey, $submitted)) {
|
||||
$_SESSION['admin_ok'] = true;
|
||||
header('Location: admin.php');
|
||||
exit;
|
||||
}
|
||||
$loginError = 'Invalid access key.';
|
||||
}
|
||||
|
||||
$authenticated = $demoMode || !empty($_SESSION['admin_ok']);
|
||||
$status = isset($_GET['status']) ? (string)$_GET['status'] : '';
|
||||
$statuses = lead_statuses();
|
||||
$leads = [];
|
||||
$counts = array_fill_keys(array_keys($statuses), 0);
|
||||
$error = null;
|
||||
if ($authenticated) {
|
||||
try {
|
||||
$leads = list_leads($status ?: null);
|
||||
$counts = lead_counts();
|
||||
} catch (Throwable $e) {
|
||||
error_log('Admin lead load failed: ' . $e->getMessage());
|
||||
$error = 'Lead data is unavailable. Check database configuration.';
|
||||
}
|
||||
}
|
||||
function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
function status_label(array $statuses, string $status): string { return $statuses[$status] ?? ucfirst($status); }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Lead Dashboard | <?= h($projectName) ?></title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content="<?= h($projectDescription) ?>">
|
||||
<meta property="og:description" content="<?= h($projectDescription) ?>">
|
||||
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
|
||||
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<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 class="admin-shell">
|
||||
<header class="site-header">
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/"><span class="brand-mark" aria-hidden="true">N</span><?= h($projectName) ?></a>
|
||||
<div class="d-flex gap-2"><a class="btn btn-outline-dark btn-sm" href="/">Home</a><?php if ($authenticated && !$demoMode): ?><a class="btn btn-dark btn-sm" href="admin.php?logout=1">Logout</a><?php endif; ?></div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="admin-hero">
|
||||
<div class="container">
|
||||
<p class="eyebrow">Admin dashboard</p>
|
||||
<h1 class="h2 mb-2">Quote requests and lead follow-up.</h1>
|
||||
<p class="text-secondary mb-0">Review incoming submissions, filter by status, and open each lead for notes.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container pb-5">
|
||||
<?php if (!$authenticated): ?>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<form class="admin-card" method="post" action="admin.php">
|
||||
<h2 class="h4">Enter admin access key</h2>
|
||||
<p class="text-secondary">Set ADMIN_ACCESS_KEY in the environment to protect this dashboard.</p>
|
||||
<?php if ($loginError): ?><div class="alert alert-danger"><?= h($loginError) ?></div><?php endif; ?>
|
||||
<label class="form-label" for="access_key">Access key</label>
|
||||
<input class="form-control" id="access_key" name="access_key" type="password" required autofocus>
|
||||
<button class="btn btn-dark mt-3 w-100" type="submit">Open dashboard</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php if ($demoMode): ?><div class="alert alert-warning">Demo admin mode is active because ADMIN_ACCESS_KEY is not set. Set it before sharing this site publicly.</div><?php endif; ?>
|
||||
<?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<div class="stat-grid mb-3">
|
||||
<?php foreach ($statuses as $key => $label): ?>
|
||||
<a class="stat text-decoration-none text-reset" href="admin.php?status=<?= h($key) ?>"><span><?= h($label) ?></span><strong><?= h((string)$counts[$key]) ?></strong></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<section class="admin-card">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h4 mb-1">Lead inbox</h2>
|
||||
<p class="text-secondary mb-0"><?= $status && isset($statuses[$status]) ? 'Filtered by ' . h($statuses[$status]) : 'Showing latest quote requests.' ?></p>
|
||||
</div>
|
||||
<div class="d-flex gap-2"><a class="btn btn-outline-dark btn-sm" href="admin.php">All leads</a><a class="btn btn-dark btn-sm" href="/#quote">Add test lead</a></div>
|
||||
</div>
|
||||
<?php if (!$leads): ?>
|
||||
<div class="empty-state"><h3>No leads yet.</h3><p>Submit the quote form on the homepage to see the workflow end-to-end.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead><tr><th>Lead</th><th>Service</th><th>Status</th><th>Created</th><th class="text-end">Action</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($leads as $lead): ?>
|
||||
<tr>
|
||||
<td><strong><?= h($lead['name']) ?></strong><br><span class="text-secondary"><?= h($lead['email']) ?><?= $lead['company'] ? ' · ' . h($lead['company']) : '' ?></span></td>
|
||||
<td><?= h($lead['service']) ?></td>
|
||||
<td><span class="badge-soft"><?= h(status_label($statuses, $lead['status'])) ?></span></td>
|
||||
<td><?= h(date('M j, Y H:i', strtotime($lead['created_at']))) ?></td>
|
||||
<td class="text-end"><a class="btn btn-outline-dark btn-sm" href="admin_lead.php?id=<?= h((string)$lead['id']) ?>">Open</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
<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>
|
||||
125
admin_lead.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/leads.php';
|
||||
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Northline Studio';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Admin lead detail for agency quote requests.';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
$accessKey = getenv('ADMIN_ACCESS_KEY') ?: '';
|
||||
$demoMode = $accessKey === '';
|
||||
$authenticated = $demoMode || !empty($_SESSION['admin_ok']);
|
||||
if (!$authenticated) {
|
||||
header('Location: admin.php');
|
||||
exit;
|
||||
}
|
||||
if (empty($_SESSION['admin_csrf'])) {
|
||||
$_SESSION['admin_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$statuses = lead_statuses();
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
|
||||
$error = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$token = (string)($_POST['csrf'] ?? '');
|
||||
if ($token === '' || !hash_equals((string)$_SESSION['admin_csrf'], $token)) {
|
||||
$error = 'The form expired. Please try again.';
|
||||
} else {
|
||||
try {
|
||||
update_lead($id, (string)($_POST['status'] ?? 'new'), (string)($_POST['admin_notes'] ?? ''));
|
||||
header('Location: admin_lead.php?id=' . $id . '&saved=1');
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
error_log('Lead update failed: ' . $e->getMessage());
|
||||
$error = 'Could not update the lead.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$lead = $id > 0 ? get_lead($id) : null;
|
||||
} catch (Throwable $e) {
|
||||
error_log('Lead detail load failed: ' . $e->getMessage());
|
||||
$lead = null;
|
||||
$error = 'Lead data is unavailable.';
|
||||
}
|
||||
function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
function status_label(array $statuses, string $status): string { return $statuses[$status] ?? ucfirst($status); }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= $lead ? 'Lead #' . h((string)$lead['id']) : 'Lead Not Found' ?> | <?= h($projectName) ?></title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content="<?= h($projectDescription) ?>">
|
||||
<meta property="og:description" content="<?= h($projectDescription) ?>">
|
||||
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
|
||||
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<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 class="admin-shell">
|
||||
<header class="site-header">
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/"><span class="brand-mark" aria-hidden="true">N</span><?= h($projectName) ?></a>
|
||||
<div class="d-flex gap-2"><a class="btn btn-outline-dark btn-sm" href="admin.php">Dashboard</a><a class="btn btn-dark btn-sm" href="/#quote">New quote</a></div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container py-4 py-md-5">
|
||||
<?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<?php if (!$lead): ?>
|
||||
<section class="admin-card empty-state"><h1 class="h3">Lead not found</h1><p>The requested lead may not exist.</p><a class="btn btn-dark" href="admin.php">Back to dashboard</a></section>
|
||||
<?php else: ?>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 mb-3">
|
||||
<div><p class="eyebrow">Lead #<?= h((string)$lead['id']) ?></p><h1 class="h2 mb-1"><?= h($lead['name']) ?></h1><p class="text-secondary mb-0"><?= h($lead['service']) ?> · <?= h(date('M j, Y H:i', strtotime($lead['created_at']))) ?></p></div>
|
||||
<span class="badge-soft align-self-md-start"><?= h(status_label($statuses, $lead['status'])) ?></span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<section class="detail-panel h-100">
|
||||
<h2 class="h4">Request details</h2>
|
||||
<div class="detail-meta my-3">
|
||||
<div><span>Email</span><a href="mailto:<?= h($lead['email']) ?>"><?= h($lead['email']) ?></a></div>
|
||||
<div><span>Phone</span><?= h($lead['phone'] ?: 'Not provided') ?></div>
|
||||
<div><span>Company</span><?= h($lead['company'] ?: 'Not provided') ?></div>
|
||||
<div><span>Budget</span><?= h($lead['budget'] ?: 'Not sure') ?></div>
|
||||
<div><span>Timeline</span><?= h($lead['timeline'] ?: 'Flexible') ?></div>
|
||||
<div><span>Source</span><?= h($lead['source'] ?: 'website') ?></div>
|
||||
</div>
|
||||
<h3 class="h5">Project notes</h3>
|
||||
<p class="mb-0"><?= nl2br(h($lead['message'])) ?></p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<form class="detail-panel needs-validation" method="post" action="admin_lead.php" novalidate>
|
||||
<input type="hidden" name="csrf" value="<?= h($_SESSION['admin_csrf']) ?>">
|
||||
<input type="hidden" name="id" value="<?= h((string)$lead['id']) ?>">
|
||||
<h2 class="h4">Follow-up</h2>
|
||||
<label class="form-label" for="status">Status</label>
|
||||
<select class="form-select mb-3" id="status" name="status">
|
||||
<?php foreach ($statuses as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>" <?= $lead['status'] === $key ? 'selected' : '' ?>><?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label class="form-label" for="admin_notes">Internal notes</label>
|
||||
<textarea class="form-control" id="admin_notes" name="admin_notes" rows="8" placeholder="Add call notes, next action, or proposal context."><?= h($lead['admin_notes'] ?? '') ?></textarea>
|
||||
<button class="btn btn-dark w-100 mt-3" type="submit">Save lead</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3"><div id="siteToast" class="toast" role="status" aria-live="polite" aria-atomic="true"><div class="toast-header"><strong class="me-auto">Dashboard</strong><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div><div class="toast-body">Saved.</div></div></div>
|
||||
<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>
|
||||
@ -1,403 +1,313 @@
|
||||
/* Agency MVP — Notion-inspired with real stock photography, fast, accessible */
|
||||
:root {
|
||||
--color-bg: #fbfaf7;
|
||||
--color-bg-dots: rgba(55, 53, 47, .08);
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-muted: #f7f6f3;
|
||||
--color-ink: #2f3437;
|
||||
--color-muted: #6f6a61;
|
||||
--color-border: #e7e2d8;
|
||||
--color-border-strong: #d8d0c2;
|
||||
--color-accent: #0f766e;
|
||||
--color-accent-soft: #e8f3ef;
|
||||
--color-warm: #f4eee4;
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 24px;
|
||||
--shadow-sm: 0 1px 0 rgba(55, 53, 47, .09), 0 8px 24px rgba(55, 53, 47, .06);
|
||||
--shadow-md: 0 1px 0 rgba(55, 53, 47, .12), 0 24px 70px rgba(55, 53, 47, .12);
|
||||
}
|
||||
|
||||
* { 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:
|
||||
radial-gradient(circle at 1px 1px, var(--color-bg-dots) 1px, transparent 0) 0 0 / 24px 24px,
|
||||
linear-gradient(180deg, #fffdf8 0%, var(--color-bg) 48%, #f7f3ec 100%);
|
||||
color: var(--color-ink);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.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;
|
||||
img { max-width: 100%; height: auto; display: block; }
|
||||
a { color: inherit; }
|
||||
a:hover { color: var(--color-accent); }
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -999px;
|
||||
top: .75rem;
|
||||
z-index: 2000;
|
||||
background: var(--color-ink);
|
||||
color: #fff;
|
||||
padding: .5rem .75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.skip-link:focus { left: .75rem; }
|
||||
|
||||
.site-header {
|
||||
background: rgba(251, 250, 247, .86);
|
||||
border-bottom: 1px solid rgba(216, 208, 194, .72);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
.navbar { padding: .78rem 0; }
|
||||
.navbar-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .65rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: -.025em;
|
||||
}
|
||||
.brand-mark {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: var(--color-ink);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.08), 0 8px 18px rgba(47,52,55,.16);
|
||||
font-size: .83rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.nav-link { color: var(--color-muted); font-weight: 650; font-size: .92rem; }
|
||||
.nav-link:hover, .nav-link:focus { color: var(--color-ink); }
|
||||
.navbar-toggler { border-color: var(--color-border); border-radius: var(--radius-sm); }
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
font-weight: 760;
|
||||
letter-spacing: -.01em;
|
||||
box-shadow: none;
|
||||
transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease, border-color .2s ease;
|
||||
}
|
||||
.btn-lg { padding: .86rem 1.08rem; font-size: .98rem; }
|
||||
.btn-dark { background: var(--color-ink); border-color: var(--color-ink); }
|
||||
.btn-dark:hover, .btn-dark:focus { background: #171a1c; border-color: #171a1c; transform: translateY(-1px); box-shadow: 0 14px 28px rgba(47,52,55,.18); }
|
||||
.btn-outline-dark { background: rgba(255,255,255,.72); border-color: var(--color-border-strong); color: var(--color-ink); }
|
||||
.btn-outline-dark:hover, .btn-outline-dark:focus { background: #fff; border-color: var(--color-ink); color: var(--color-ink); transform: translateY(-1px); }
|
||||
|
||||
.section-pad { padding: 82px 0; }
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.hero::before,
|
||||
.hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-radius: 999px;
|
||||
filter: blur(6px);
|
||||
opacity: .72;
|
||||
}
|
||||
.hero::before { width: 320px; height: 320px; right: -120px; top: 80px; background: #e8f3ef; }
|
||||
.hero::after { width: 260px; height: 260px; left: -90px; bottom: 20px; background: #f3e8d8; }
|
||||
.hero .container { position: relative; z-index: 1; }
|
||||
.eyebrow, .small-label, .card-kicker {
|
||||
color: var(--color-accent);
|
||||
font-size: .75rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: .1em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: .68rem;
|
||||
}
|
||||
.emoji {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: .32rem;
|
||||
border-radius: 7px;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-warm);
|
||||
border: 1px solid var(--color-border);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
h1, h2, h3 { letter-spacing: -.055em; line-height: 1.04; }
|
||||
h1 { max-width: 780px; font-size: clamp(2.65rem, 6vw, 5.85rem); font-weight: 820; }
|
||||
h2 { font-size: clamp(1.9rem, 3.2vw, 3rem); font-weight: 790; }
|
||||
h3 { font-size: 1.13rem; font-weight: 780; }
|
||||
.hero-copy, .section-copy, .section-heading p { color: var(--color-muted); max-width: 680px; font-size: 1.06rem; }
|
||||
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: .8rem;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255,255,255,.7);
|
||||
border: 1px solid rgba(216, 208, 194, .86);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.hero-visual img { border-radius: 18px; aspect-ratio: 4 / 3; object-fit: cover; width: 100%; }
|
||||
.sticky-note {
|
||||
position: absolute;
|
||||
right: 1.35rem;
|
||||
bottom: 1.25rem;
|
||||
max-width: 245px;
|
||||
color: #4b4035;
|
||||
background: #fff3bf;
|
||||
border: 1px solid #eedf9a;
|
||||
border-radius: 13px;
|
||||
padding: .85rem .95rem;
|
||||
box-shadow: 0 16px 30px rgba(84, 68, 35, .16);
|
||||
transform: rotate(-1.5deg);
|
||||
font-size: .91rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
.note-pin {
|
||||
position: absolute;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
right: 16px;
|
||||
top: 12px;
|
||||
border-radius: 50%;
|
||||
background: #e2624b;
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
.proof-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: .75rem;
|
||||
max-width: 640px;
|
||||
}
|
||||
.proof-grid div, .service-card, .case-card, .quote-card, .lead-form, .notice-box, .admin-card, .detail-panel {
|
||||
background: rgba(255,255,255,.86);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.proof-grid div { padding: .92rem; }
|
||||
.proof-grid dt { font-size: 1.48rem; font-weight: 850; letter-spacing: -.05em; }
|
||||
.proof-grid dd { color: var(--color-muted); margin: 0; font-size: .84rem; }
|
||||
.status-pill, .badge-soft {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background: var(--color-surface-muted);
|
||||
color: var(--color-ink);
|
||||
border-radius: 999px;
|
||||
padding: .24rem .55rem;
|
||||
font-size: .74rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.check-list { list-style: none; padding: 0; margin: 1rem 0 0; }
|
||||
.check-list li { position: relative; padding: .62rem 0 .62rem 1.45rem; border-top: 1px solid var(--color-border); }
|
||||
.check-list li::before { content: ""; position: absolute; left: 0; top: 1rem; width: 8px; height: 8px; border-radius: 50%; background: var(--color-accent); }
|
||||
|
||||
.logo-strip { padding: 30px 0; background: rgba(255,255,255,.7); border-bottom: 1px solid var(--color-border); border-top: 1px solid var(--color-border); }
|
||||
.logos { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .75rem; }
|
||||
.logos span { color: #4d4740; border: 1px solid var(--color-border); border-radius: 999px; padding: .72rem; text-align: center; font-weight: 800; font-size: .86rem; background: #fffdf8; }
|
||||
.section-heading { max-width: 760px; margin-bottom: 1.9rem; }
|
||||
.section-heading.compact { margin: 0; }
|
||||
.service-card, .case-card { padding: .86rem; overflow: hidden; transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease; }
|
||||
.service-card:hover, .case-card:hover { transform: translateY(-3px); border-color: var(--color-border-strong); box-shadow: var(--shadow-md); }
|
||||
.card-illustration, .case-image {
|
||||
width: 100%;
|
||||
height: 218px;
|
||||
object-fit: cover;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface-muted);
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.photo-credit {
|
||||
color: var(--color-muted);
|
||||
font-size: .72rem;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 .85rem;
|
||||
}
|
||||
.photo-credit a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.hero-credit {
|
||||
position: absolute;
|
||||
left: 1.3rem;
|
||||
bottom: 1.22rem;
|
||||
margin: 0;
|
||||
padding: .34rem .55rem;
|
||||
border: 1px solid rgba(216, 208, 194, .76);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 253, 248, .82);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.service-card span { display: inline-block; color: var(--color-muted); font-weight: 850; margin-bottom: .65rem; }
|
||||
.service-card p, .case-card p, .quote-card { color: var(--color-muted); }
|
||||
.muted-section { background: rgba(247, 246, 243, .78); border: 1px solid var(--color-border); border-left: 0; border-right: 0; }
|
||||
.metric { color: var(--color-ink) !important; font-size: 2.2rem; line-height: 1; font-weight: 900; letter-spacing: -.07em; margin-bottom: .55rem; }
|
||||
.quote-card { padding: 1.25rem; height: 100%; margin: 0; }
|
||||
.quote-card footer { color: var(--color-ink); font-weight: 800; margin-top: .9rem; }
|
||||
.quote-avatar {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 12px;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-warm);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: .78rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.quote-section { background: rgba(255,255,255,.65); border-top: 1px solid var(--color-border); }
|
||||
.notice-box { padding: .95rem; color: var(--color-muted); background: var(--color-surface-muted); font-size: .9rem; }
|
||||
.lead-form { padding: 1.15rem; }
|
||||
.form-label { font-size: .84rem; font-weight: 800; color: #4d4740; }
|
||||
.form-control, .form-select {
|
||||
border-color: var(--color-border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: .76rem .82rem;
|
||||
background-color: #fffdf8;
|
||||
}
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 .2rem rgba(15, 118, 110, .14);
|
||||
}
|
||||
textarea.form-control { resize: vertical; }
|
||||
.form-note { color: var(--color-muted); font-size: .9rem; }
|
||||
.site-footer { padding: 28px 0; color: var(--color-muted); border-top: 1px solid var(--color-border); background: rgba(255,255,255,.78); }
|
||||
.site-footer a { color: var(--color-ink); font-weight: 750; text-decoration: none; }
|
||||
|
||||
.admin-shell { min-height: 100vh; background: var(--color-bg); }
|
||||
.admin-hero { padding: 42px 0 24px; }
|
||||
.admin-card, .detail-panel { padding: 1rem; }
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .7rem; }
|
||||
.stat { padding: .85rem; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-md); }
|
||||
.stat strong { display: block; font-size: 1.4rem; letter-spacing: -.04em; }
|
||||
.table { --bs-table-bg: transparent; vertical-align: middle; }
|
||||
.table thead th { color: var(--color-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; border-bottom-color: var(--color-border-strong); }
|
||||
.table td { border-color: var(--color-border); }
|
||||
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--color-muted); }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .75rem; }
|
||||
.detail-meta div { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: .75rem; background: var(--color-surface-muted); }
|
||||
.detail-meta span { display: block; color: var(--color-muted); font-size: .78rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
|
||||
.toast { border-radius: var(--radius-md); border: 1px solid var(--color-border); }
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.section-pad { padding: 58px 0; }
|
||||
.logos { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.sticky-note { position: relative; right: auto; bottom: auto; max-width: none; margin: .9rem .2rem 0; }
|
||||
.hero-credit { left: 1.2rem; bottom: 6.3rem; }
|
||||
}
|
||||
|
||||
.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;
|
||||
@media (max-width: 575.98px) {
|
||||
h1 { font-size: clamp(2.35rem, 14vw, 3.45rem); }
|
||||
.proof-grid { grid-template-columns: 1fr; }
|
||||
.detail-meta { grid-template-columns: 1fr; }
|
||||
.lead-form { padding: 1rem; }
|
||||
.logos { grid-template-columns: 1fr; }
|
||||
.card-illustration, .case-image { height: 205px; }
|
||||
.hero-credit { position: static; display: inline-block; margin: .65rem .2rem 0; }
|
||||
}
|
||||
|
||||
.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;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; }
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-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;
|
||||
}
|
||||
BIN
assets/images/landing/case-agency-team.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
assets/images/landing/case-agency.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="520" height="300" viewBox="0 0 520 300" role="img" aria-labelledby="title desc"><title id="title">Agency analytics</title><desc id="desc">Analytics cards and a quote form preview.</desc><rect width="520" height="300" rx="28" fill="#FFF3BF"/><rect x="54" y="44" width="412" height="212" rx="24" fill="#fff" stroke="#EEDC86" stroke-width="2"/><rect x="86" y="78" width="112" height="16" rx="8" fill="#2F3437"/><rect x="86" y="118" width="158" height="98" rx="18" fill="#F7F6F3" stroke="#DED8CD"/><rect x="276" y="118" width="158" height="98" rx="18" fill="#F7F6F3" stroke="#DED8CD"/><path d="M108 190l36-34 34 18 42-46" fill="none" stroke="#0F766E" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/><rect x="304" y="144" width="102" height="11" rx="6" fill="#D8D0C2"/><rect x="304" y="171" width="74" height="11" rx="6" fill="#D8D0C2"/><rect x="304" y="196" width="84" height="12" rx="6" fill="#2F3437"/></svg>
|
||||
|
After Width: | Height: | Size: 970 B |
BIN
assets/images/landing/case-consulting-meeting.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
1
assets/images/landing/case-consulting.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="520" height="300" viewBox="0 0 520 300" role="img" aria-labelledby="title desc"><title id="title">Consulting pipeline</title><desc id="desc">A consulting lead pipeline with organized columns.</desc><rect width="520" height="300" rx="28" fill="#F4EEE4"/><rect x="50" y="46" width="420" height="208" rx="24" fill="#fff" stroke="#E2DACE" stroke-width="2"/><g fill="#F7F6F3" stroke="#DED8CD" stroke-width="2"><rect x="82" y="88" width="96" height="126" rx="18"/><rect x="212" y="88" width="96" height="126" rx="18"/><rect x="342" y="88" width="96" height="126" rx="18"/></g><rect x="104" y="115" width="52" height="10" rx="5" fill="#2F3437"/><rect x="104" y="145" width="48" height="38" rx="12" fill="#FFF3BF"/><rect x="234" y="115" width="52" height="10" rx="5" fill="#2F3437"/><rect x="234" y="145" width="48" height="38" rx="12" fill="#E8F3EF"/><rect x="364" y="115" width="52" height="10" rx="5" fill="#2F3437"/><rect x="364" y="145" width="48" height="38" rx="12" fill="#0F766E" opacity=".86"/><path d="M172 151h42M302 151h42" stroke="#2F3437" stroke-width="6" stroke-linecap="round"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/landing/case-fintech-dashboard.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
1
assets/images/landing/case-fintech.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="520" height="300" viewBox="0 0 520 300" role="img" aria-labelledby="title desc"><title id="title">Fintech case dashboard</title><desc id="desc">Finance dashboard with rising chart and KPI cards.</desc><rect width="520" height="300" rx="28" fill="#E8F3EF"/><rect x="54" y="44" width="412" height="212" rx="24" fill="#fff" stroke="#CFE3DC" stroke-width="2"/><rect x="86" y="78" width="132" height="16" rx="8" fill="#2F3437"/><rect x="86" y="120" width="72" height="92" rx="15" fill="#F7F6F3"/><rect x="180" y="96" width="72" height="116" rx="15" fill="#F7F6F3"/><rect x="274" y="138" width="72" height="74" rx="15" fill="#F7F6F3"/><rect x="368" y="80" width="72" height="132" rx="15" fill="#0F766E" opacity=".88"/><path d="M90 222c72-48 105-88 161-69 47 16 92 31 181-57" fill="none" stroke="#2F3437" stroke-width="8" stroke-linecap="round"/></svg>
|
||||
|
After Width: | Height: | Size: 892 B |
1
assets/images/landing/growth-board.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="520" height="320" viewBox="0 0 520 320" role="img" aria-labelledby="title desc"><title id="title">Growth board</title><desc id="desc">Analytics chart and campaign cards.</desc><rect width="520" height="320" rx="28" fill="#F7F6F3"/><rect x="56" y="54" width="408" height="212" rx="24" fill="#fff" stroke="#DED8CD" stroke-width="2"/><rect x="86" y="86" width="112" height="16" rx="8" fill="#2F3437"/><rect x="86" y="124" width="98" height="92" rx="18" fill="#F4EEE4" stroke="#E2DACE"/><rect x="210" y="124" width="98" height="92" rx="18" fill="#E8F3EF" stroke="#CFE3DC"/><rect x="334" y="124" width="98" height="92" rx="18" fill="#FFF3BF" stroke="#EEDC86"/><path d="M102 205c38-44 78-37 108-62 39-32 75 10 107-10 28-17 48-54 91-48" fill="none" stroke="#0F766E" stroke-width="10" stroke-linecap="round"/><circle cx="102" cy="205" r="9" fill="#0F766E"/><circle cx="210" cy="143" r="9" fill="#0F766E"/><circle cx="317" cy="133" r="9" fill="#0F766E"/><circle cx="408" cy="85" r="9" fill="#0F766E"/><rect x="86" y="236" width="120" height="12" rx="6" fill="#D8D0C2"/><rect x="226" y="236" width="92" height="12" rx="6" fill="#D8D0C2"/><rect x="338" y="236" width="70" height="12" rx="6" fill="#D8D0C2"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/landing/hero-agency-workspace.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
1
assets/images/landing/launch-page.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="520" height="320" viewBox="0 0 520 320" role="img" aria-labelledby="title desc"><title id="title">Landing page wireframe</title><desc id="desc">A clean website page with call to action and content cards.</desc><rect width="520" height="320" rx="28" fill="#F7F6F3"/><rect x="58" y="48" width="404" height="224" rx="24" fill="#fff" stroke="#DED8CD" stroke-width="2"/><rect x="58" y="48" width="404" height="42" rx="24" fill="#FAF9F5"/><circle cx="84" cy="69" r="6" fill="#E2624B"/><circle cx="104" cy="69" r="6" fill="#E7B85A"/><circle cx="124" cy="69" r="6" fill="#58A874"/><rect x="91" y="120" width="178" height="20" rx="10" fill="#2F3437"/><rect x="91" y="154" width="218" height="12" rx="6" fill="#D8D0C2"/><rect x="91" y="180" width="118" height="34" rx="12" fill="#2F3437"/><rect x="326" y="118" width="92" height="92" rx="22" fill="#E8F3EF" stroke="#CFE3DC"/><path d="M352 172l24-31 24 45h-64z" fill="#0F766E" opacity=".9"/><rect x="91" y="232" width="78" height="14" rx="7" fill="#D8D0C2"/><rect x="189" y="232" width="78" height="14" rx="7" fill="#D8D0C2"/><rect x="287" y="232" width="78" height="14" rx="7" fill="#D8D0C2"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
assets/images/landing/research-notes.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="520" height="320" viewBox="0 0 520 320" role="img" aria-labelledby="title desc"><title id="title">Offer notes</title><desc id="desc">Strategy notes and highlighted offer blocks.</desc><rect width="520" height="320" rx="28" fill="#F7F6F3"/><rect x="76" y="44" width="286" height="232" rx="22" fill="#fff" stroke="#DED8CD" stroke-width="2"/><rect x="105" y="80" width="138" height="16" rx="8" fill="#2F3437"/><rect x="105" y="119" width="202" height="12" rx="6" fill="#D8D0C2"/><rect x="105" y="148" width="162" height="12" rx="6" fill="#D8D0C2"/><rect x="105" y="177" width="210" height="48" rx="14" fill="#FFF3BF" stroke="#EEDC86"/><path d="M365 93l59 59-109 109-67 8 8-67z" fill="#E8F3EF" stroke="#0F766E" stroke-width="5" stroke-linejoin="round"/><path d="M365 93l59 59" stroke="#0F766E" stroke-width="5" stroke-linecap="round"/><circle cx="403" cy="78" r="28" fill="#2F3437"/><path d="M391 79h25M404 66v25" stroke="#fff" stroke-width="6" stroke-linecap="round"/></svg>
|
||||
|
After Width: | Height: | Size: 1018 B |
BIN
assets/images/landing/service-campaign-dashboard.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/images/landing/service-leadgen-website.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/images/landing/service-positioning-workshop.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
1
assets/images/landing/workspace-dashboard.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="720" viewBox="0 0 960 720" role="img" aria-labelledby="title desc"><title id="title">Lead pipeline workspace</title><desc id="desc">A clean workspace dashboard with lead cards, charts, and notes.</desc><rect width="960" height="720" rx="34" fill="#F7F6F3"/><rect x="64" y="58" width="832" height="604" rx="28" fill="#fff" stroke="#DCD6CB" stroke-width="2"/><rect x="64" y="58" width="832" height="64" rx="28" fill="#FAF9F5"/><circle cx="100" cy="91" r="8" fill="#E2624B"/><circle cx="126" cy="91" r="8" fill="#E7B85A"/><circle cx="152" cy="91" r="8" fill="#58A874"/><rect x="190" y="80" width="246" height="22" rx="11" fill="#EEEAE1"/><rect x="98" y="156" width="164" height="426" rx="20" fill="#F7F3EC" stroke="#E2DACE"/><rect x="125" y="187" width="86" height="12" rx="6" fill="#2F3437"/><rect x="125" y="225" width="105" height="12" rx="6" fill="#D8D0C2"/><rect x="125" y="257" width="78" height="12" rx="6" fill="#D8D0C2"/><rect x="125" y="289" width="112" height="12" rx="6" fill="#D8D0C2"/><rect x="125" y="516" width="94" height="34" rx="11" fill="#2F3437"/><rect x="298" y="156" width="518" height="126" rx="20" fill="#E8F3EF" stroke="#CFE3DC"/><rect x="328" y="187" width="156" height="18" rx="9" fill="#2F3437"/><rect x="328" y="222" width="250" height="12" rx="6" fill="#6F6A61" opacity=".62"/><rect x="328" y="246" width="184" height="12" rx="6" fill="#6F6A61" opacity=".38"/><path d="M636 244c30-74 72-69 96-20 21 43 47 42 70-18" fill="none" stroke="#0F766E" stroke-width="12" stroke-linecap="round"/><circle cx="636" cy="244" r="10" fill="#0F766E"/><circle cx="732" cy="224" r="10" fill="#0F766E"/><circle cx="802" cy="206" r="10" fill="#0F766E"/><g transform="translate(298 314)"><rect width="158" height="214" rx="19" fill="#FDFBF6" stroke="#E2DACE"/><rect x="22" y="25" width="78" height="13" rx="7" fill="#2F3437"/><rect x="22" y="60" width="112" height="44" rx="13" fill="#F4EEE4" stroke="#E2DACE"/><rect x="22" y="120" width="112" height="44" rx="13" fill="#fff" stroke="#E2DACE"/></g><g transform="translate(478 314)"><rect width="158" height="214" rx="19" fill="#FDFBF6" stroke="#E2DACE"/><rect x="22" y="25" width="93" height="13" rx="7" fill="#2F3437"/><rect x="22" y="60" width="112" height="44" rx="13" fill="#E8F3EF" stroke="#CFE3DC"/><rect x="22" y="120" width="112" height="44" rx="13" fill="#fff" stroke="#E2DACE"/></g><g transform="translate(658 314)"><rect width="158" height="214" rx="19" fill="#FDFBF6" stroke="#E2DACE"/><rect x="22" y="25" width="72" height="13" rx="7" fill="#2F3437"/><rect x="22" y="60" width="112" height="44" rx="13" fill="#FFF3BF" stroke="#EEDC86"/><rect x="22" y="120" width="112" height="44" rx="13" fill="#fff" stroke="#E2DACE"/></g><rect x="546" y="560" width="270" height="54" rx="18" fill="#2F3437"/><rect x="577" y="580" width="118" height="14" rx="7" fill="#fff" opacity=".92"/><rect x="716" y="580" width="58" height="14" rx="7" fill="#fff" opacity=".32"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@ -1,39 +1,43 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const toastEl = document.getElementById('siteToast');
|
||||
const showToast = (message) => {
|
||||
if (!toastEl || !window.bootstrap) return;
|
||||
const body = toastEl.querySelector('.toast-body');
|
||||
if (body) body.textContent = message;
|
||||
window.bootstrap.Toast.getOrCreateInstance(toastEl, { delay: 3600 }).show();
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
document.querySelectorAll('a[href^="#"]').forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
const target = document.querySelector(link.getAttribute('href'));
|
||||
if (!target) return;
|
||||
event.preventDefault();
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.pushState(null, '', link.getAttribute('href'));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.needs-validation').forEach((form) => {
|
||||
form.addEventListener('submit', (event) => {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
showToast('Please review the highlighted fields before submitting.');
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
});
|
||||
|
||||
if (window.location.hash === '#quote') {
|
||||
const firstInvalid = document.querySelector('.is-invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.focus({ preventScroll: true });
|
||||
showToast('A few fields need attention.');
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.get('saved') === '1') {
|
||||
showToast('Lead updated.');
|
||||
}
|
||||
});
|
||||
|
||||
4
healthz.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => true, 'time' => gmdate('c'), 'php' => PHP_VERSION], JSON_UNESCAPED_SLASHES);
|
||||
164
includes/leads.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
function e(?string $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function ensure_leads_table(): void
|
||||
{
|
||||
$sql = "CREATE TABLE IF NOT EXISTS agency_leads (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
email VARCHAR(190) NOT NULL,
|
||||
phone VARCHAR(60) DEFAULT NULL,
|
||||
company VARCHAR(140) DEFAULT NULL,
|
||||
service VARCHAR(120) NOT NULL,
|
||||
budget VARCHAR(80) DEFAULT NULL,
|
||||
timeline VARCHAR(80) DEFAULT NULL,
|
||||
message TEXT NOT NULL,
|
||||
status VARCHAR(40) NOT NULL DEFAULT 'new',
|
||||
admin_notes TEXT DEFAULT NULL,
|
||||
source VARCHAR(120) DEFAULT 'website',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status_created (status, created_at),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||
db()->exec($sql);
|
||||
}
|
||||
|
||||
function lead_statuses(): array
|
||||
{
|
||||
return [
|
||||
'new' => 'New',
|
||||
'reviewing' => 'Reviewing',
|
||||
'contacted' => 'Contacted',
|
||||
'proposal' => 'Proposal sent',
|
||||
'won' => 'Won',
|
||||
'lost' => 'Lost',
|
||||
];
|
||||
}
|
||||
|
||||
function sanitize_text(string $value, int $maxLength): string
|
||||
{
|
||||
$value = trim(preg_replace('/\s+/', ' ', $value) ?? '');
|
||||
if (strlen($value) > $maxLength) {
|
||||
$value = substr($value, 0, $maxLength);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
function validate_lead_payload(array $input): array
|
||||
{
|
||||
$data = [
|
||||
'name' => sanitize_text((string)($input['name'] ?? ''), 120),
|
||||
'email' => sanitize_text((string)($input['email'] ?? ''), 190),
|
||||
'phone' => sanitize_text((string)($input['phone'] ?? ''), 60),
|
||||
'company' => sanitize_text((string)($input['company'] ?? ''), 140),
|
||||
'service' => sanitize_text((string)($input['service'] ?? ''), 120),
|
||||
'budget' => sanitize_text((string)($input['budget'] ?? ''), 80),
|
||||
'timeline' => sanitize_text((string)($input['timeline'] ?? ''), 80),
|
||||
'message' => trim((string)($input['message'] ?? '')),
|
||||
'source' => sanitize_text((string)($input['source'] ?? 'website'), 120),
|
||||
];
|
||||
|
||||
if (strlen($data['message']) > 3000) {
|
||||
$data['message'] = substr($data['message'], 0, 3000);
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
if ($data['name'] === '') {
|
||||
$errors['name'] = 'Please enter your name.';
|
||||
}
|
||||
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['email'] = 'Please enter a valid email address.';
|
||||
}
|
||||
if ($data['service'] === '') {
|
||||
$errors['service'] = 'Please choose a service.';
|
||||
}
|
||||
if (strlen($data['message']) < 20) {
|
||||
$errors['message'] = 'Please share at least 20 characters about the project.';
|
||||
}
|
||||
|
||||
return [$data, $errors];
|
||||
}
|
||||
|
||||
function create_lead(array $data): int
|
||||
{
|
||||
ensure_leads_table();
|
||||
$stmt = db()->prepare('INSERT INTO agency_leads (name, email, phone, company, service, budget, timeline, message, source) VALUES (:name, :email, :phone, :company, :service, :budget, :timeline, :message, :source)');
|
||||
$stmt->execute([
|
||||
':name' => $data['name'],
|
||||
':email' => $data['email'],
|
||||
':phone' => $data['phone'] !== '' ? $data['phone'] : null,
|
||||
':company' => $data['company'] !== '' ? $data['company'] : null,
|
||||
':service' => $data['service'],
|
||||
':budget' => $data['budget'] !== '' ? $data['budget'] : null,
|
||||
':timeline' => $data['timeline'] !== '' ? $data['timeline'] : null,
|
||||
':message' => $data['message'],
|
||||
':source' => $data['source'],
|
||||
]);
|
||||
return (int)db()->lastInsertId();
|
||||
}
|
||||
|
||||
function list_leads(?string $status = null, int $limit = 100): array
|
||||
{
|
||||
ensure_leads_table();
|
||||
$statuses = lead_statuses();
|
||||
if ($status && isset($statuses[$status])) {
|
||||
$stmt = db()->prepare('SELECT * FROM agency_leads WHERE status = :status ORDER BY created_at DESC LIMIT :limit');
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
$stmt = db()->prepare('SELECT * FROM agency_leads ORDER BY created_at DESC LIMIT :limit');
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function get_lead(int $id): ?array
|
||||
{
|
||||
ensure_leads_table();
|
||||
$stmt = db()->prepare('SELECT * FROM agency_leads WHERE id = :id LIMIT 1');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$lead = $stmt->fetch();
|
||||
return $lead ?: null;
|
||||
}
|
||||
|
||||
function update_lead(int $id, string $status, string $notes): void
|
||||
{
|
||||
$statuses = lead_statuses();
|
||||
if (!isset($statuses[$status])) {
|
||||
$status = 'new';
|
||||
}
|
||||
$notes = trim($notes);
|
||||
if (strlen($notes) > 3000) {
|
||||
$notes = substr($notes, 0, 3000);
|
||||
}
|
||||
ensure_leads_table();
|
||||
$stmt = db()->prepare('UPDATE agency_leads SET status = :status, admin_notes = :notes WHERE id = :id');
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':notes', $notes !== '' ? $notes : null, $notes !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function lead_counts(): array
|
||||
{
|
||||
ensure_leads_table();
|
||||
$counts = array_fill_keys(array_keys(lead_statuses()), 0);
|
||||
$stmt = db()->query('SELECT status, COUNT(*) AS total FROM agency_leads GROUP BY status');
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
if (isset($counts[$row['status']])) {
|
||||
$counts[$row['status']] = (int)$row['total'];
|
||||
}
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
417
index.php
@ -1,150 +1,315 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
session_start();
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if (empty($_SESSION['lead_csrf'])) {
|
||||
$_SESSION['lead_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Northline Studio';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'A clean, conversion-focused agency website for strategy, design, and growth teams.';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
$errors = $_SESSION['lead_errors'] ?? [];
|
||||
$old = $_SESSION['lead_old'] ?? [];
|
||||
$flash = $_SESSION['lead_flash'] ?? null;
|
||||
$brandInitial = strtoupper(substr(trim((string)$projectName), 0, 1) ?: 'N');
|
||||
unset($_SESSION['lead_errors'], $_SESSION['lead_old'], $_SESSION['lead_flash']);
|
||||
|
||||
function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
function old_value(array $old, string $key): string { return h((string)($old[$key] ?? '')); }
|
||||
|
||||
$heroPhoto = [
|
||||
'src' => 'assets/images/landing/hero-agency-workspace.jpg',
|
||||
'alt' => 'Finance agency team reviewing charts and laptops in a bright office workspace',
|
||||
'credit' => 'www.kaboompics.com',
|
||||
'url' => 'https://www.pexels.com/@karola-g',
|
||||
];
|
||||
|
||||
$services = [
|
||||
['01', 'Positioning & offer', 'Clarify your audience, promise, service packages, and proof so visitors understand why you are the safe choice.', 'assets/images/landing/service-positioning-workshop.jpg', 'Business team organizing strategy notes during a positioning workshop', 'Jakub Zerdzicki', 'https://www.pexels.com/@jakubzerdzicki'],
|
||||
['02', 'Lead-gen websites', 'Fast, accessible landing pages with SEO foundations, strong CTAs, and high-intent quote forms.', 'assets/images/landing/service-leadgen-website.jpg', 'Laptop workspace used for web design and lead generation planning', 'Negative Space', 'https://www.pexels.com/@negativespace'],
|
||||
['03', 'Campaign systems', 'Paid search, nurture copy, CRM handoff, and analytics dashboards that keep sales teams focused.', 'assets/images/landing/service-campaign-dashboard.jpg', 'Marketing analytics dashboard open on a laptop for campaign reporting', 'Lukas Blazek', 'https://www.pexels.com/@goumbik'],
|
||||
];
|
||||
|
||||
$caseStudies = [
|
||||
['+68%', 'SaaS demo requests', 'Rebuilt the offer hierarchy and quote path for a workflow automation platform.', 'assets/images/landing/case-fintech-dashboard.jpg', 'Fintech analytics dashboard reviewed on a laptop for growth reporting', 'Jakub Zerdzicki', 'https://www.pexels.com/@jakubzerdzicki'],
|
||||
['3.1x', 'Qualified pipeline', 'Launched a focused paid search landing system for a cybersecurity consultancy.', 'assets/images/landing/case-consulting-meeting.jpg', 'Consulting team meeting with notebooks and laptops around a table', 'Vitaly Gariev', 'https://www.pexels.com/@silverkblack'],
|
||||
['-37%', 'Cost per lead', 'Refined service pages, proof blocks, and form routing for a regional agency.', 'assets/images/landing/case-agency-team.jpg', 'Agency team collaborating during a planning meeting in a modern office', 'Ivan S', 'https://www.pexels.com/@ivan-s'],
|
||||
];
|
||||
?>
|
||||
<!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'] ?? '';
|
||||
?>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= h($projectName) ?> | Agency Strategy, Design & Growth</title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<meta name="description" content="<?= h($projectDescription) ?>">
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<meta property="og:description" content="<?= h($projectDescription) ?>">
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="twitter:image" content="<?= h($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>
|
||||
<meta name="author" content="<?= h($projectName) ?>">
|
||||
<meta property="og:title" content="<?= h($projectName) ?> | Agency Strategy, Design & Growth">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<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=2026052903">
|
||||
</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>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<header class="site-header sticky-top">
|
||||
<nav class="navbar navbar-expand-lg" aria-label="Primary navigation">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/" aria-label="<?= h($projectName) ?> home">
|
||||
<span class="brand-mark" aria-hidden="true"><?= h($brandInitial) ?></span>
|
||||
<span><?= h($projectName) ?></span>
|
||||
</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" href="#services">Services</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#case-studies">Case studies</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#testimonials">Testimonials</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="admin.php">Admin</a></li>
|
||||
<li class="nav-item"><a class="btn btn-dark btn-sm ms-lg-2" href="#quote">Request quote</a></li>
|
||||
</ul>
|
||||
</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>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<?php if ($flash): ?>
|
||||
<div class="container pt-3">
|
||||
<div class="alert alert-<?= h($flash['type'] ?? 'info') ?> alert-dismissible fade show" role="alert">
|
||||
<?= h($flash['message'] ?? '') ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="hero section-pad">
|
||||
<div class="container">
|
||||
<div class="row align-items-center g-4 g-xl-5">
|
||||
<div class="col-lg-6">
|
||||
<p class="eyebrow"><span class="emoji" aria-hidden="true">✦</span> Strategy-led digital agency</p>
|
||||
<h1>Turn your offer into a clean lead engine.</h1>
|
||||
<p class="hero-copy">We design fast, editorial landing pages and quote flows that help service teams explain value, earn trust, and convert better-fit prospects.</p>
|
||||
<div class="d-flex flex-column flex-sm-row gap-2 mt-4">
|
||||
<a href="#quote" class="btn btn-dark btn-lg">Get a quote</a>
|
||||
<a href="#case-studies" class="btn btn-outline-dark btn-lg">View results</a>
|
||||
</div>
|
||||
<dl class="proof-grid mt-4" aria-label="Agency performance highlights">
|
||||
<div><dt>42%</dt><dd>avg. lead quality lift</dd></div>
|
||||
<div><dt>21d</dt><dd>typical launch sprint</dd></div>
|
||||
<div><dt>96</dt><dd>client NPS</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<figure class="hero-visual" aria-label="Lead generation workspace preview">
|
||||
<img src="<?= h($heroPhoto['src']) ?>" width="1880" height="1253" alt="<?= h($heroPhoto['alt']) ?>" fetchpriority="high">
|
||||
<p class="photo-credit hero-credit">Photo: <a href="<?= h($heroPhoto['url']) ?>" target="_blank" rel="noopener"><?= h($heroPhoto['credit']) ?></a></p>
|
||||
<figcaption class="sticky-note">
|
||||
<span class="note-pin" aria-hidden="true"></span>
|
||||
Free 48-hour funnel snapshot included with every quote request.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="logo-strip" aria-label="Partner logos">
|
||||
<div class="container">
|
||||
<p class="small-label">Trusted by growing teams and platform partners</p>
|
||||
<div class="logos">
|
||||
<span>Vectorly</span><span>SummitOps</span><span>Bright CRM</span><span>Northstar</span><span>Ledgerly</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-pad" id="services">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow"><span class="emoji" aria-hidden="true">▣</span> Services</p>
|
||||
<h2>Clean execution across the lead journey.</h2>
|
||||
<p>Pick one sprint or combine services into a complete conversion program.</p>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($services as $service): ?>
|
||||
<div class="col-md-4">
|
||||
<article class="service-card h-100">
|
||||
<img class="card-illustration" src="<?= h($service[3]) ?>" width="940" height="627" alt="<?= h($service[4]) ?>" loading="lazy">
|
||||
<p class="photo-credit">Photo: <a href="<?= h($service[6]) ?>" target="_blank" rel="noopener"><?= h($service[5]) ?></a></p>
|
||||
<span><?= h($service[0]) ?></span>
|
||||
<h3><?= h($service[1]) ?></h3>
|
||||
<p><?= h($service[2]) ?></p>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-pad muted-section" id="case-studies">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow"><span class="emoji" aria-hidden="true">↗</span> Case studies</p>
|
||||
<h2>Recent work with practical outcomes.</h2>
|
||||
<p>Short, focused builds with measurable pipeline impact.</p>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($caseStudies as $case): ?>
|
||||
<div class="col-lg-4">
|
||||
<article class="case-card h-100">
|
||||
<img class="case-image" src="<?= h($case[3]) ?>" width="940" height="627" alt="<?= h($case[4]) ?>" loading="lazy">
|
||||
<p class="photo-credit">Photo: <a href="<?= h($case[6]) ?>" target="_blank" rel="noopener"><?= h($case[5]) ?></a></p>
|
||||
<p class="metric"><?= h($case[0]) ?></p>
|
||||
<h3><?= h($case[1]) ?></h3>
|
||||
<p><?= h($case[2]) ?></p>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-pad" id="testimonials">
|
||||
<div class="container">
|
||||
<div class="row g-3 align-items-stretch">
|
||||
<div class="col-lg-5">
|
||||
<div class="section-heading compact">
|
||||
<p class="eyebrow"><span class="emoji" aria-hidden="true">❝</span> Testimonials</p>
|
||||
<h2>Trusted when clarity and speed matter.</h2>
|
||||
<p>Senior teams choose us when they need a lean partner that can ship and measure quickly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<blockquote class="quote-card">
|
||||
<div class="quote-avatar" aria-hidden="true">MR</div>
|
||||
“They turned a vague service story into a page our sales team actually uses.”
|
||||
<footer>— Maya R., VP Marketing</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<blockquote class="quote-card">
|
||||
<div class="quote-avatar" aria-hidden="true">DK</div>
|
||||
“The lead quality improved because the site finally explained who we were for.”
|
||||
<footer>— Daniel K., Founder</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-pad quote-section" id="quote">
|
||||
<div class="container">
|
||||
<div class="row g-4 g-xl-5">
|
||||
<div class="col-lg-5">
|
||||
<p class="eyebrow"><span class="emoji" aria-hidden="true">✉</span> Request a quote</p>
|
||||
<h2>Tell us what you want to improve.</h2>
|
||||
<p class="section-copy">Your submission is saved to the admin dashboard for follow-up. If MAIL_TO is configured, the site also attempts an email notification.</p>
|
||||
<div class="notice-box" role="note">Testing notice: configure your own SMTP and MAIL_TO values for reliable production email delivery.</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<form action="quote.php" method="post" class="lead-form needs-validation" novalidate>
|
||||
<input type="hidden" name="csrf" value="<?= h($_SESSION['lead_csrf']) ?>">
|
||||
<input type="hidden" name="source" value="homepage_quote_form">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="name">Name *</label>
|
||||
<input class="form-control <?= isset($errors['name']) ? 'is-invalid' : '' ?>" id="name" name="name" value="<?= old_value($old, 'name') ?>" autocomplete="name" required>
|
||||
<div class="invalid-feedback"><?= h($errors['name'] ?? 'Please enter your name.') ?></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">Work email *</label>
|
||||
<input type="email" class="form-control <?= isset($errors['email']) ? 'is-invalid' : '' ?>" id="email" name="email" value="<?= old_value($old, 'email') ?>" autocomplete="email" required>
|
||||
<div class="invalid-feedback"><?= h($errors['email'] ?? 'Please enter a valid email.') ?></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="company">Company</label>
|
||||
<input class="form-control" id="company" name="company" value="<?= old_value($old, 'company') ?>" autocomplete="organization">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Phone</label>
|
||||
<input class="form-control" id="phone" name="phone" value="<?= old_value($old, 'phone') ?>" autocomplete="tel">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="service">Service *</label>
|
||||
<select class="form-select <?= isset($errors['service']) ? 'is-invalid' : '' ?>" id="service" name="service" required>
|
||||
<option value="">Choose one</option>
|
||||
<?php foreach (['Positioning & offer', 'Lead-gen website', 'Campaign system', 'Full growth sprint'] as $option): ?>
|
||||
<option value="<?= h($option) ?>" <?= (($old['service'] ?? '') === $option) ? 'selected' : '' ?>><?= h($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="invalid-feedback"><?= h($errors['service'] ?? 'Please choose a service.') ?></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="budget">Budget</label>
|
||||
<select class="form-select" id="budget" name="budget">
|
||||
<option value="">Not sure</option>
|
||||
<?php foreach (['Under $5k', '$5k–$15k', '$15k–$30k', '$30k+'] as $option): ?>
|
||||
<option value="<?= h($option) ?>" <?= (($old['budget'] ?? '') === $option) ? 'selected' : '' ?>><?= h($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="timeline">Timeline</label>
|
||||
<select class="form-select" id="timeline" name="timeline">
|
||||
<option value="">Flexible</option>
|
||||
<?php foreach (['ASAP', 'This month', 'This quarter', 'Planning ahead'] as $option): ?>
|
||||
<option value="<?= h($option) ?>" <?= (($old['timeline'] ?? '') === $option) ? 'selected' : '' ?>><?= h($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="message">Project notes *</label>
|
||||
<textarea class="form-control <?= isset($errors['message']) ? 'is-invalid' : '' ?>" id="message" name="message" rows="5" minlength="20" required placeholder="What are you trying to improve? What should happen after launch?"><?= old_value($old, 'message') ?></textarea>
|
||||
<div class="invalid-feedback"><?= h($errors['message'] ?? 'Please share at least 20 characters.') ?></div>
|
||||
</div>
|
||||
<div class="col-12 d-flex flex-column flex-sm-row align-items-sm-center gap-3">
|
||||
<button class="btn btn-dark btn-lg" type="submit">Submit quote request</button>
|
||||
<p class="form-note mb-0">No spam. We reply with next steps, not a newsletter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container d-flex flex-column flex-md-row justify-content-between gap-2">
|
||||
<p class="mb-0">© <?= date('Y') ?> <?= h($projectName) ?>. Built for clear conversion.</p>
|
||||
<p class="mb-0"><a href="admin.php">Admin dashboard</a> · <a href="#quote">Request quote</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="siteToast" class="toast" role="status" aria-live="polite" aria-atomic="true">
|
||||
<div class="toast-header"><strong class="me-auto">Notice</strong><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div>
|
||||
<div class="toast-body">Ready.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
53
quote.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/includes/leads.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /#quote');
|
||||
exit;
|
||||
}
|
||||
|
||||
$token = (string)($_POST['csrf'] ?? '');
|
||||
if ($token === '' || empty($_SESSION['lead_csrf']) || !hash_equals((string)$_SESSION['lead_csrf'], $token)) {
|
||||
$_SESSION['lead_flash'] = ['type' => 'danger', 'message' => 'The form expired. Please try again.'];
|
||||
header('Location: /#quote');
|
||||
exit;
|
||||
}
|
||||
|
||||
[$data, $errors] = validate_lead_payload($_POST);
|
||||
if ($errors) {
|
||||
$_SESSION['lead_errors'] = $errors;
|
||||
$_SESSION['lead_old'] = $data;
|
||||
header('Location: /#quote');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$leadId = create_lead($data);
|
||||
$_SESSION['lead_csrf'] = bin2hex(random_bytes(32));
|
||||
|
||||
$mailStatus = 'not_configured';
|
||||
$mailPath = __DIR__ . '/mail/MailService.php';
|
||||
if (is_file($mailPath)) {
|
||||
require_once $mailPath;
|
||||
$emailBody = "Service: {$data['service']}\nBudget: {$data['budget']}\nTimeline: {$data['timeline']}\nCompany: {$data['company']}\nPhone: {$data['phone']}\n\n{$data['message']}\n\nLead ID: {$leadId}";
|
||||
$res = MailService::sendContactMessage($data['name'], $data['email'], $emailBody, null, 'New agency quote request');
|
||||
$mailStatus = !empty($res['success']) ? 'sent' : 'not_configured';
|
||||
if (empty($res['success'])) {
|
||||
error_log('Lead email notification skipped/failed: ' . ($res['error'] ?? 'unknown'));
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['last_lead_id'] = $leadId;
|
||||
$_SESSION['last_mail_status'] = $mailStatus;
|
||||
header('Location: thank-you.php?id=' . $leadId);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
error_log('Lead save failed: ' . $e->getMessage());
|
||||
$_SESSION['lead_old'] = $data;
|
||||
$_SESSION['lead_flash'] = ['type' => 'danger', 'message' => 'We could not save your request yet. Please try again in a moment.'];
|
||||
header('Location: /#quote');
|
||||
exit;
|
||||
}
|
||||
54
thank-you.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Northline Studio';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Thanks for requesting a quote. Your request has been received.';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
$leadId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$sessionLeadId = (int)($_SESSION['last_lead_id'] ?? 0);
|
||||
$mailStatus = (string)($_SESSION['last_mail_status'] ?? 'not_configured');
|
||||
function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Quote Request Received | <?= h($projectName) ?></title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content="<?= h($projectDescription) ?>">
|
||||
<meta property="og:description" content="<?= h($projectDescription) ?>">
|
||||
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
|
||||
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<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>
|
||||
<main class="admin-shell d-flex align-items-center">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-7">
|
||||
<section class="admin-card p-4 p-md-5 text-center">
|
||||
<p class="eyebrow">Request received</p>
|
||||
<h1 class="h2">Thanks — your quote request is in the dashboard.</h1>
|
||||
<p class="text-secondary mt-3">We’ll review the details and follow up with next steps. Your reference is <strong>#<?= h((string)($leadId ?: $sessionLeadId)) ?></strong>.</p>
|
||||
<?php if ($mailStatus !== 'sent'): ?>
|
||||
<div class="notice-box mt-3 text-start">Testing notice: the request was saved, but email delivery depends on MAIL_TO and SMTP settings. Flatlogic does not guarantee usage of the default mail server; configure your own SMTP in environment variables for production.</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-center gap-2 mt-4">
|
||||
<a class="btn btn-dark" href="/">Back to homepage</a>
|
||||
<a class="btn btn-outline-dark" href="admin.php">Open admin dashboard</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||