diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..2d86290 --- /dev/null +++ b/admin.php @@ -0,0 +1,142 @@ +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); } +?> + + + + + + Lead Dashboard | <?= h($projectName) ?> + + + + + + + + + + + + + + + + +
+
+
+

Admin dashboard

+

Quote requests and lead follow-up.

+

Review incoming submissions, filter by status, and open each lead for notes.

+
+
+ +
+ +
+
+
+

Enter admin access key

+

Set ADMIN_ACCESS_KEY in the environment to protect this dashboard.

+
+ + + +
+
+
+ +
Demo admin mode is active because ADMIN_ACCESS_KEY is not set. Set it before sharing this site publicly.
+
+
+ $label): ?> + + +
+
+
+
+

Lead inbox

+

+
+ +
+ +

No leads yet.

Submit the quote form on the homepage to see the workflow end-to-end.

+ +
+ + + + + + + + + + + + + +
LeadServiceStatusCreatedAction

Open
+
+ +
+ +
+
+ + + + diff --git a/admin_lead.php b/admin_lead.php new file mode 100644 index 0000000..cc1d7d5 --- /dev/null +++ b/admin_lead.php @@ -0,0 +1,125 @@ +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); } +?> + + + + + + <?= $lead ? 'Lead #' . h((string)$lead['id']) : 'Lead Not Found' ?> | <?= h($projectName) ?> + + + + + + + + + + + + + + + +
+
+ +

Lead not found

The requested lead may not exist.

Back to dashboard
+ +
+

Lead #

·

+ +
+
+
+
+

Request details

+
+
Email
+
Phone
+
Company
+
Budget
+
Timeline
+
Source
+
+

Project notes

+

+
+
+
+
+ + +

Follow-up

+ + + + + +
+
+
+ +
+
Dashboard
Saved.
+ + + + diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..f92b196 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -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; -} \ No newline at end of file diff --git a/assets/images/landing/case-agency-team.jpg b/assets/images/landing/case-agency-team.jpg new file mode 100644 index 0000000..4c899aa Binary files /dev/null and b/assets/images/landing/case-agency-team.jpg differ diff --git a/assets/images/landing/case-agency.svg b/assets/images/landing/case-agency.svg new file mode 100644 index 0000000..40df8df --- /dev/null +++ b/assets/images/landing/case-agency.svg @@ -0,0 +1 @@ +Agency analyticsAnalytics cards and a quote form preview. \ No newline at end of file diff --git a/assets/images/landing/case-consulting-meeting.jpg b/assets/images/landing/case-consulting-meeting.jpg new file mode 100644 index 0000000..6ef4d6a Binary files /dev/null and b/assets/images/landing/case-consulting-meeting.jpg differ diff --git a/assets/images/landing/case-consulting.svg b/assets/images/landing/case-consulting.svg new file mode 100644 index 0000000..8176af4 --- /dev/null +++ b/assets/images/landing/case-consulting.svg @@ -0,0 +1 @@ +Consulting pipelineA consulting lead pipeline with organized columns. \ No newline at end of file diff --git a/assets/images/landing/case-fintech-dashboard.jpg b/assets/images/landing/case-fintech-dashboard.jpg new file mode 100644 index 0000000..08bce10 Binary files /dev/null and b/assets/images/landing/case-fintech-dashboard.jpg differ diff --git a/assets/images/landing/case-fintech.svg b/assets/images/landing/case-fintech.svg new file mode 100644 index 0000000..48a32df --- /dev/null +++ b/assets/images/landing/case-fintech.svg @@ -0,0 +1 @@ +Fintech case dashboardFinance dashboard with rising chart and KPI cards. \ No newline at end of file diff --git a/assets/images/landing/growth-board.svg b/assets/images/landing/growth-board.svg new file mode 100644 index 0000000..3e2e7b0 --- /dev/null +++ b/assets/images/landing/growth-board.svg @@ -0,0 +1 @@ +Growth boardAnalytics chart and campaign cards. \ No newline at end of file diff --git a/assets/images/landing/hero-agency-workspace.jpg b/assets/images/landing/hero-agency-workspace.jpg new file mode 100644 index 0000000..a08bbf5 Binary files /dev/null and b/assets/images/landing/hero-agency-workspace.jpg differ diff --git a/assets/images/landing/launch-page.svg b/assets/images/landing/launch-page.svg new file mode 100644 index 0000000..7aebe9b --- /dev/null +++ b/assets/images/landing/launch-page.svg @@ -0,0 +1 @@ +Landing page wireframeA clean website page with call to action and content cards. \ No newline at end of file diff --git a/assets/images/landing/research-notes.svg b/assets/images/landing/research-notes.svg new file mode 100644 index 0000000..38c8cd5 --- /dev/null +++ b/assets/images/landing/research-notes.svg @@ -0,0 +1 @@ +Offer notesStrategy notes and highlighted offer blocks. \ No newline at end of file diff --git a/assets/images/landing/service-campaign-dashboard.jpg b/assets/images/landing/service-campaign-dashboard.jpg new file mode 100644 index 0000000..3eb490f Binary files /dev/null and b/assets/images/landing/service-campaign-dashboard.jpg differ diff --git a/assets/images/landing/service-leadgen-website.jpg b/assets/images/landing/service-leadgen-website.jpg new file mode 100644 index 0000000..0a0a9bd Binary files /dev/null and b/assets/images/landing/service-leadgen-website.jpg differ diff --git a/assets/images/landing/service-positioning-workshop.jpg b/assets/images/landing/service-positioning-workshop.jpg new file mode 100644 index 0000000..9c33842 Binary files /dev/null and b/assets/images/landing/service-positioning-workshop.jpg differ diff --git a/assets/images/landing/workspace-dashboard.svg b/assets/images/landing/workspace-dashboard.svg new file mode 100644 index 0000000..46f1091 --- /dev/null +++ b/assets/images/landing/workspace-dashboard.svg @@ -0,0 +1 @@ +Lead pipeline workspaceA clean workspace dashboard with lead cards, charts, and notes. \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..c14e91c 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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.'); + } }); diff --git a/healthz.php b/healthz.php new file mode 100644 index 0000000..286e5e9 --- /dev/null +++ b/healthz.php @@ -0,0 +1,4 @@ + true, 'time' => gmdate('c'), 'php' => PHP_VERSION], JSON_UNESCAPED_SLASHES); diff --git a/includes/leads.php b/includes/leads.php new file mode 100644 index 0000000..7044eb2 --- /dev/null +++ b/includes/leads.php @@ -0,0 +1,164 @@ +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; +} diff --git a/index.php b/index.php index 7205f3d..22935b5 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,315 @@ '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'], +]; ?> - - - New Style - + + + <?= h($projectName) ?> | Agency Strategy, Design & Growth - + - + - + - + - + - - - - + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + +
+ + + +
+ +
+ +
+ + +
+
+
+
+

Strategy-led digital agency

+

Turn your offer into a clean lead engine.

+

We design fast, editorial landing pages and quote flows that help service teams explain value, earn trust, and convert better-fit prospects.

+ +
+
42%
avg. lead quality lift
+
21d
typical launch sprint
+
96
client NPS
+
+
+
+
+ <?= h($heroPhoto['alt']) ?> +

Photo:

+
+ + Free 48-hour funnel snapshot included with every quote request. +
+
+
+
+
+
+ +
+
+

Trusted by growing teams and platform partners

+
+ VectorlySummitOpsBright CRMNorthstarLedgerly +
+
+
+ +
+
+
+

Services

+

Clean execution across the lead journey.

+

Pick one sprint or combine services into a complete conversion program.

+
+
+ +
+
+ <?= h($service[4]) ?> +

Photo:

+ +

+

+
+
+ +
+
+
+ +
+
+
+

Case studies

+

Recent work with practical outcomes.

+

Short, focused builds with measurable pipeline impact.

+
+
+ +
+
+ <?= h($case[4]) ?> +

Photo:

+

+

+

+
+
+ +
+
+
+ +
+
+
+
+
+

Testimonials

+

Trusted when clarity and speed matter.

+

Senior teams choose us when they need a lean partner that can ship and measure quickly.

+
+
+
+
+
+
+ + “They turned a vague service story into a page our sales team actually uses.” +
— Maya R., VP Marketing
+
+
+
+
+ + “The lead quality improved because the site finally explained who we were for.” +
— Daniel K., Founder
+
+
+
+
+
+
+
+ +
+
+
+
+

Request a quote

+

Tell us what you want to improve.

+

Your submission is saved to the admin dashboard for follow-up. If MAIL_TO is configured, the site also attempts an email notification.

+
Testing notice: configure your own SMTP and MAIL_TO values for reliable production email delivery.
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +

No spam. We reply with next steps, not a newsletter.

+
+
+
+
+
+
+
-