Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeb101f333 |
@ -1,403 +1,150 @@
|
||||
:root {
|
||||
--color-bg: #f7f7f5;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-2: #eeeeeb;
|
||||
--color-text: #181817;
|
||||
--color-muted: #686865;
|
||||
--color-border: #d9d9d3;
|
||||
--color-accent: #111111;
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
--shadow-sm: 0 1px 2px rgba(20, 20, 20, .05);
|
||||
--shadow-md: 0 12px 30px rgba(20, 20, 20, .08);
|
||||
--space-section: clamp(3rem, 7vw, 6rem);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
a { color: inherit; text-underline-offset: 3px; }
|
||||
a:hover { color: #000; }
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
.site-header {
|
||||
background: rgba(247, 247, 245, .92);
|
||||
backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.navbar { padding-block: .75rem; }
|
||||
.navbar-brand { font-weight: 700; letter-spacing: -.03em; }
|
||||
.nav-link { color: var(--color-muted); font-weight: 600; font-size: .92rem; }
|
||||
.nav-link.active, .nav-link:hover { color: var(--color-text); }
|
||||
.navbar-toggler { border-radius: var(--radius-sm); border-color: var(--color-border); }
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
.btn {
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
letter-spacing: -.01em;
|
||||
padding: .72rem 1rem;
|
||||
}
|
||||
.btn-lg { padding: .86rem 1.15rem; font-size: .98rem; }
|
||||
.btn-dark { background: var(--color-accent); border-color: var(--color-accent); }
|
||||
.btn-outline-dark { border-color: #2c2c2a; }
|
||||
.btn:focus-visible, .form-control:focus, .form-select:focus {
|
||||
box-shadow: 0 0 0 .22rem rgba(24, 24, 23, .14);
|
||||
border-color: var(--color-text);
|
||||
}
|
||||
|
||||
.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;
|
||||
.hero-section { padding: clamp(3rem, 8vw, 6.5rem) 0 var(--space-section); }
|
||||
.eyebrow {
|
||||
color: var(--color-muted);
|
||||
font-size: .76rem;
|
||||
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;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
h1, h2, h3, .page-title {
|
||||
color: var(--color-text);
|
||||
letter-spacing: -.045em;
|
||||
line-height: 1.05;
|
||||
}
|
||||
h1 { font-size: clamp(2.35rem, 6vw, 4.75rem); font-weight: 800; max-width: 11ch; }
|
||||
h2 { font-size: clamp(1.65rem, 3.5vw, 2.75rem); font-weight: 800; }
|
||||
h3 { font-size: 1.05rem; font-weight: 800; }
|
||||
.page-title { font-size: clamp(2rem, 4vw, 3.25rem); font-weight: 800; }
|
||||
.lead-copy { color: var(--color-muted); font-size: clamp(1rem, 2vw, 1.14rem); max-width: 42rem; }
|
||||
|
||||
.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;
|
||||
.metric-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: .75rem;
|
||||
max-width: 34rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
.metric-row div, .feature-card, .panel, .form-shell, .confirmation-card, .table-card, .detail-card, .empty-state {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.metric-row div { padding: .9rem; }
|
||||
.metric-row dt { font-size: 1.1rem; font-weight: 800; line-height: 1; }
|
||||
.metric-row dd { margin: .25rem 0 0; color: var(--color-muted); font-size: .82rem; }
|
||||
|
||||
.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;
|
||||
.panel { padding: 1rem; }
|
||||
.hero-panel { max-width: 34rem; margin-left: auto; }
|
||||
.panel-header {
|
||||
display: flex; align-items: center; gap: .5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: .85rem; margin-bottom: 1rem;
|
||||
color: var(--color-muted); font-size: .86rem; font-weight: 700;
|
||||
}
|
||||
.status-dot { width: .55rem; height: .55rem; border-radius: 50%; background: #198754; display: inline-block; }
|
||||
.preview-card { background: var(--color-surface-2); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 1rem; margin-bottom: 1rem; }
|
||||
.preview-line { height: .7rem; background: #cfcfca; border-radius: 999px; margin-bottom: .65rem; }
|
||||
.preview-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem; margin-top: 1rem; }
|
||||
.preview-grid span { min-height: 4.7rem; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-sm); }
|
||||
.check-list { list-style: none; padding: 0; display: grid; gap: .55rem; color: var(--color-muted); }
|
||||
.check-list li { position: relative; padding-left: 1.35rem; }
|
||||
.check-list li::before { content: "✓"; position: absolute; left: 0; color: var(--color-text); font-weight: 900; }
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
}
|
||||
.section { padding: var(--space-section) 0; }
|
||||
.muted-section { background: var(--color-surface-2); border-block: 1px solid var(--color-border); }
|
||||
.section-heading { max-width: 44rem; margin-bottom: 1.5rem; }
|
||||
.section-heading p:not(.eyebrow) { color: var(--color-muted); }
|
||||
.feature-card { padding: 1.25rem; height: 100%; }
|
||||
.feature-card p { color: var(--color-muted); margin-bottom: 0; }
|
||||
.timeline-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
|
||||
.timeline-list li { display: grid; grid-template-columns: 3rem 1fr; gap: 1rem; padding: 1rem; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
|
||||
.timeline-list span { font-weight: 900; color: var(--color-muted); }
|
||||
.timeline-list p { margin: .2rem 0 0; color: var(--color-muted); }
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.form-shell, .confirmation-card, .detail-card, .empty-state { padding: clamp(1.2rem, 3vw, 2rem); }
|
||||
.form-label { font-weight: 700; font-size: .9rem; }
|
||||
.form-control, .form-select { border-radius: var(--radius-sm); border-color: var(--color-border); padding: .78rem .86rem; }
|
||||
.hp-field { position: absolute; left: -10000px; width: 1px; height: 1px; opacity: 0; }
|
||||
.invalid-feedback { font-size: .8rem; }
|
||||
|
||||
.header-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.alert { border-radius: var(--radius-sm); border: 1px solid var(--color-border); }
|
||||
.confirmation-page { min-height: 64vh; display: flex; align-items: center; }
|
||||
.submitted-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; }
|
||||
.submitted-summary div { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: .9rem; background: var(--color-bg); }
|
||||
.submitted-summary span { display: block; color: var(--color-muted); font-size: .78rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
|
||||
.submitted-summary strong { display: block; margin-top: .18rem; overflow-wrap: anywhere; }
|
||||
|
||||
.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);
|
||||
}
|
||||
.table-card { overflow: hidden; }
|
||||
.table { --bs-table-bg: transparent; }
|
||||
.table th { color: var(--color-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; }
|
||||
.table td, .table th { padding: 1rem; border-color: var(--color-border); }
|
||||
.empty-state { text-align: center; padding-block: 4rem; }
|
||||
.empty-state p { color: var(--color-muted); }
|
||||
.back-link { color: var(--color-muted); font-weight: 700; text-decoration: none; }
|
||||
.back-link:hover { color: var(--color-text); }
|
||||
.detail-layout { max-width: 940px; }
|
||||
.message-box { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 1rem; white-space: normal; }
|
||||
|
||||
.admin-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.site-footer { padding: 1.5rem 0; border-top: 1px solid var(--color-border); color: var(--color-muted); font-size: .88rem; }
|
||||
|
||||
.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;
|
||||
@media (max-width: 767.98px) {
|
||||
h1 { max-width: none; }
|
||||
.metric-row, .submitted-summary { grid-template-columns: 1fr; }
|
||||
.hero-panel { margin-left: 0; }
|
||||
.table td, .table th { white-space: nowrap; }
|
||||
}
|
||||
@ -1,39 +1,26 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
(() => {
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
forms.forEach((form) => {
|
||||
form.addEventListener('submit', (event) => {
|
||||
const message = form.querySelector('textarea[name="message"]');
|
||||
if (message && message.value.trim().length < 10) {
|
||||
message.setCustomValidity('Please enter at least 10 characters.');
|
||||
} else if (message) {
|
||||
message.setCustomValidity('');
|
||||
}
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
});
|
||||
|
||||
const textarea = document.querySelector('textarea[name="message"]');
|
||||
const counter = document.querySelector('[data-char-count]');
|
||||
if (textarea && counter) {
|
||||
const update = () => { counter.textContent = String(textarea.value.length); };
|
||||
textarea.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
})();
|
||||
|
||||
78
contact.php
Normal file
78
contact.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
require_once __DIR__ . '/mail/MailService.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: index.php#lead-form');
|
||||
exit;
|
||||
}
|
||||
|
||||
function back_with_error(string $message): never
|
||||
{
|
||||
header('Location: index.php?error=' . urlencode($message) . '#lead-form');
|
||||
exit;
|
||||
}
|
||||
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
$company = trim((string)($_POST['company'] ?? ''));
|
||||
$budget = trim((string)($_POST['budget'] ?? ''));
|
||||
$message = trim((string)($_POST['message'] ?? ''));
|
||||
$honeypot = trim((string)($_POST['website'] ?? ''));
|
||||
|
||||
if ($honeypot !== '') {
|
||||
header('Location: thank-you.php');
|
||||
exit;
|
||||
}
|
||||
if ($name === '' || strlen($name) > 120) {
|
||||
back_with_error('Name is required and must be under 120 characters.');
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 190) {
|
||||
back_with_error('A valid email address is required.');
|
||||
}
|
||||
if (strlen($message) < 10 || strlen($message) > 2000) {
|
||||
back_with_error('Message must be between 10 and 2000 characters.');
|
||||
}
|
||||
if (strlen($company) > 160 || strlen($budget) > 80) {
|
||||
back_with_error('One of the optional fields is too long.');
|
||||
}
|
||||
|
||||
try {
|
||||
ensure_leads_table();
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
$agent = substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255);
|
||||
|
||||
$stmt = db()->prepare('INSERT INTO leads (public_token, name, email, company, budget, message, source, ip_address, user_agent) VALUES (:token, :name, :email, :company, :budget, :message, :source, :ip, :agent)');
|
||||
$stmt->bindValue(':token', $token);
|
||||
$stmt->bindValue(':name', $name);
|
||||
$stmt->bindValue(':email', $email);
|
||||
$stmt->bindValue(':company', $company !== '' ? $company : null);
|
||||
$stmt->bindValue(':budget', $budget !== '' ? $budget : null);
|
||||
$stmt->bindValue(':message', $message);
|
||||
$stmt->bindValue(':source', 'Landing page');
|
||||
$stmt->bindValue(':ip', $ip);
|
||||
$stmt->bindValue(':agent', $agent);
|
||||
$stmt->execute();
|
||||
$leadId = (int)db()->lastInsertId();
|
||||
|
||||
$safeName = e($name);
|
||||
$safeEmail = e($email);
|
||||
$safeMessage = nl2br(e($message));
|
||||
$html = "<h2>New landing page lead</h2><p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><p><strong>Company:</strong> " . e($company ?: 'Not provided') . "</p><p><strong>Budget:</strong> " . e($budget ?: 'Not sure') . "</p><p><strong>Message:</strong><br>{$safeMessage}</p>";
|
||||
$text = "New landing page lead\nName: {$name}\nEmail: {$email}\nCompany: " . ($company ?: 'Not provided') . "\nBudget: " . ($budget ?: 'Not sure') . "\n\n{$message}";
|
||||
$mailResult = MailService::sendMail(null, 'New landing page lead from ' . $name, $html, $text, ['reply_to' => $email]);
|
||||
if (!empty($mailResult['success'])) {
|
||||
$update = db()->prepare('UPDATE leads SET email_sent = 1 WHERE id = :id');
|
||||
$update->bindValue(':id', $leadId, PDO::PARAM_INT);
|
||||
$update->execute();
|
||||
} else {
|
||||
error_log('Lead notification email failed: ' . ($mailResult['error'] ?? 'unknown error'));
|
||||
}
|
||||
|
||||
header('Location: thank-you.php?token=' . urlencode($token));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Lead submission failed: ' . $exception->getMessage());
|
||||
back_with_error('We could not save your request right now. Please try again in a moment.');
|
||||
}
|
||||
9
healthz/index.php
Normal file
9
healthz/index.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'service' => 'landing-leads',
|
||||
'time' => gmdate('c'),
|
||||
'php' => PHP_VERSION,
|
||||
], JSON_UNESCAPED_SLASHES);
|
||||
167
includes/app.php
Normal file
167
includes/app.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function e(?string $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function project_name(): string
|
||||
{
|
||||
$name = $_SERVER['PROJECT_NAME'] ?? $_SERVER['APP_NAME'] ?? getenv('PROJECT_NAME') ?: '';
|
||||
return $name !== '' ? (string)$name : 'LaunchPage';
|
||||
}
|
||||
|
||||
function short_text(?string $value, int $limit = 54): string
|
||||
{
|
||||
$text = trim((string)$value);
|
||||
if (strlen($text) <= $limit) {
|
||||
return $text;
|
||||
}
|
||||
return rtrim(substr($text, 0, max(0, $limit - 3))) . '…';
|
||||
}
|
||||
|
||||
function project_description(): string
|
||||
{
|
||||
$description = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: '';
|
||||
return $description !== '' ? $description : 'A focused landing page with a fast, secure lead capture workflow.';
|
||||
}
|
||||
|
||||
function ensure_leads_table(): void
|
||||
{
|
||||
static $ready = false;
|
||||
if ($ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
$sql = "CREATE TABLE IF NOT EXISTS leads (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
public_token CHAR(32) NOT NULL UNIQUE,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
email VARCHAR(190) NOT NULL,
|
||||
company VARCHAR(160) NULL,
|
||||
budget VARCHAR(80) NULL,
|
||||
message TEXT NOT NULL,
|
||||
source VARCHAR(120) NULL,
|
||||
status ENUM('new','contacted','closed') NOT NULL DEFAULT 'new',
|
||||
email_sent TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ip_address VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||
db()->exec($sql);
|
||||
$ready = true;
|
||||
}
|
||||
|
||||
function lead_count(): int
|
||||
{
|
||||
ensure_leads_table();
|
||||
$stmt = db()->query('SELECT COUNT(*) AS total FROM leads');
|
||||
return (int)($stmt->fetch()['total'] ?? 0);
|
||||
}
|
||||
|
||||
function latest_leads(int $limit = 8): array
|
||||
{
|
||||
ensure_leads_table();
|
||||
$stmt = db()->prepare('SELECT id, public_token, name, email, company, budget, message, status, email_sent, created_at FROM leads ORDER BY created_at DESC LIMIT :limit');
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function fetch_lead_by_token(string $token): ?array
|
||||
{
|
||||
ensure_leads_table();
|
||||
$stmt = db()->prepare('SELECT * FROM leads WHERE public_token = :token LIMIT 1');
|
||||
$stmt->bindValue(':token', $token, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
$lead = $stmt->fetch();
|
||||
return $lead ?: null;
|
||||
}
|
||||
|
||||
function fetch_lead_by_id(int $id): ?array
|
||||
{
|
||||
ensure_leads_table();
|
||||
$stmt = db()->prepare('SELECT * FROM leads WHERE id = :id LIMIT 1');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$lead = $stmt->fetch();
|
||||
return $lead ?: null;
|
||||
}
|
||||
|
||||
function page_head(string $title, string $description = ''): void
|
||||
{
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? getenv('PROJECT_IMAGE_URL') ?: '';
|
||||
$metaDescription = $description !== '' ? $description : ($projectDescription !== '' ? $projectDescription : project_description());
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= e($title) ?></title>
|
||||
<meta name="description" content="<?= e($metaDescription) ?>">
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description from project environment -->
|
||||
<meta property="og:description" content="<?= e($projectDescription) ?>">
|
||||
<meta property="twitter:description" content="<?= e($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Platform-managed preview image -->
|
||||
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
|
||||
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<meta property="og:title" content="<?= e($title) ?>">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=2026052901">
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
}
|
||||
|
||||
function page_nav(string $active = 'home'): void
|
||||
{
|
||||
?>
|
||||
<header class="site-header sticky-top">
|
||||
<nav class="navbar navbar-expand-lg" aria-label="Primary navigation">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="index.php" aria-label="<?= e(project_name()) ?> home"><?= e(project_name()) ?></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="mainNav">
|
||||
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
|
||||
<li class="nav-item"><a class="nav-link <?= $active === 'home' ? 'active' : '' ?>" href="index.php#offer">Offer</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?= $active === 'process' ? 'active' : '' ?>" href="index.php#process">Process</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?= $active === 'leads' ? 'active' : '' ?>" href="leads.php">Leads</a></li>
|
||||
<li class="nav-item"><a class="btn btn-dark btn-sm ms-lg-2" href="index.php#lead-form">Request info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<?php
|
||||
}
|
||||
|
||||
function page_footer(): void
|
||||
{
|
||||
$year = date('Y');
|
||||
?>
|
||||
<footer class="site-footer">
|
||||
<div class="container d-flex flex-column flex-md-row justify-content-between gap-2">
|
||||
<p class="mb-0">© <?= e((string)$year) ?> <?= e(project_name()) ?>. Built for fast lead capture.</p>
|
||||
<p class="mb-0"><a href="leads.php">View leads</a> <span aria-hidden="true">·</span> <a href="/healthz">Health</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=2026052901" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
293
index.php
293
index.php
@ -1,150 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$sent = isset($_GET['sent']) && $_GET['sent'] === '1';
|
||||
$error = isset($_GET['error']) ? (string)$_GET['error'] : '';
|
||||
$leadTotal = 0;
|
||||
try {
|
||||
$leadTotal = lead_count();
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Lead count unavailable: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
page_head(project_name() . ' — Landing Page & Lead Capture', project_description());
|
||||
page_nav('home');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<section class="hero-section" id="top">
|
||||
<div class="container">
|
||||
<?php if ($sent): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">
|
||||
<strong>Request received.</strong> Thanks — your details were saved and the team can follow up shortly.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<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>
|
||||
<?php elseif ($error): ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
|
||||
<strong>Please review the form.</strong> <?= e($error) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row align-items-center g-4 g-lg-5">
|
||||
<div class="col-lg-6">
|
||||
<p class="eyebrow mb-3">Professional landing page hosting</p>
|
||||
<h1>Publish your offer and capture qualified leads in one focused page.</h1>
|
||||
<p class="lead-copy">A restrained, conversion-ready landing shell with a secure contact workflow, confirmation page, and lead review area — ready to replace with your uploaded HTML whenever you provide it.</p>
|
||||
<div class="hero-actions d-flex flex-column flex-sm-row gap-2 mt-4">
|
||||
<a class="btn btn-dark btn-lg" href="#lead-form">Request a quote</a>
|
||||
<a class="btn btn-outline-dark btn-lg" href="leads.php">Review leads</a>
|
||||
</div>
|
||||
<dl class="metric-row mt-4" aria-label="Landing page metrics">
|
||||
<div><dt><?= e((string)$leadTotal) ?></dt><dd>stored leads</dd></div>
|
||||
<div><dt>24h</dt><dd>fast response target</dd></div>
|
||||
<div><dt>PDO</dt><dd>secure storage</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<aside class="panel hero-panel" aria-label="Lead capture preview">
|
||||
<div class="panel-header">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<span>Lead workflow active</span>
|
||||
</div>
|
||||
<div class="preview-card">
|
||||
<div class="preview-line w-75"></div>
|
||||
<div class="preview-line w-50"></div>
|
||||
<div class="preview-grid">
|
||||
<span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="check-list mb-0">
|
||||
<li>Server-side validation</li>
|
||||
<li>Spam honeypot protection</li>
|
||||
<li>Database-backed lead list and detail view</li>
|
||||
<li>Email notification attempted via configured mail service</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="offer">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">What is included</p>
|
||||
<h2>A complete first slice, not just a placeholder.</h2>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><article class="feature-card"><h3>Landing message</h3><p>Clear positioning, compact proof points, and focused calls-to-action for visitors.</p></article></div>
|
||||
<div class="col-md-4"><article class="feature-card"><h3>Lead form</h3><p>Name, email, company, budget, and message with accessible validation and smooth feedback.</p></article></div>
|
||||
<div class="col-md-4"><article class="feature-card"><h3>Lead review</h3><p>Submissions are saved to MariaDB and available in a concise list and detail page.</p></article></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section muted-section" id="process">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-5">
|
||||
<p class="eyebrow">Visitor journey</p>
|
||||
<h2>From interest to follow-up in under a minute.</h2>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<ol class="timeline-list">
|
||||
<li><span>01</span><div><strong>Understand the offer</strong><p>Visitors get a concise promise and proof points immediately.</p></div></li>
|
||||
<li><span>02</span><div><strong>Submit details</strong><p>The form validates required fields and blocks common bot noise.</p></div></li>
|
||||
<li><span>03</span><div><strong>Confirm and review</strong><p>The lead is stored, a notification is attempted, and the lead detail is available.</p></div></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="lead-form">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9 col-xl-8">
|
||||
<div class="form-shell">
|
||||
<div class="section-heading text-center mb-4">
|
||||
<p class="eyebrow">Contact / lead form</p>
|
||||
<h2>Tell us what you want to launch.</h2>
|
||||
<p>Submissions are saved securely. Email delivery depends on your configured SMTP or MAIL_TO environment settings.</p>
|
||||
</div>
|
||||
<form class="needs-validation" action="contact.php" method="post" novalidate data-lead-form>
|
||||
<input type="text" name="website" class="hp-field" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input id="name" name="name" type="text" class="form-control" maxlength="120" required autocomplete="name">
|
||||
<div class="invalid-feedback">Please enter your name.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input id="email" name="email" type="email" class="form-control" maxlength="190" required autocomplete="email">
|
||||
<div class="invalid-feedback">Please enter a valid email.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="company" class="form-label">Company <span class="text-muted">optional</span></label>
|
||||
<input id="company" name="company" type="text" class="form-control" maxlength="160" autocomplete="organization">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="budget" class="form-label">Budget range <span class="text-muted">optional</span></label>
|
||||
<select id="budget" name="budget" class="form-select">
|
||||
<option value="">Not sure yet</option>
|
||||
<option value="Under $1,000">Under $1,000</option>
|
||||
<option value="$1,000–$5,000">$1,000–$5,000</option>
|
||||
<option value="$5,000–$10,000">$5,000–$10,000</option>
|
||||
<option value="$10,000+">$10,000+</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="message" class="form-label">What do you need?</label>
|
||||
<textarea id="message" name="message" class="form-control" rows="5" maxlength="2000" required placeholder="Share the landing page goal, offer, audience, or timeline."></textarea>
|
||||
<div class="d-flex justify-content-between mt-1"><div class="invalid-feedback d-block">Please enter at least 10 characters.</div><small class="text-muted"><span data-char-count>0</span>/2000</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-3 mt-4">
|
||||
<button class="btn btn-dark btn-lg" type="submit">Send request</button>
|
||||
<p class="small text-muted mb-0">Testing notice: Flatlogic does not guarantee mail server usage. Configure your own SMTP in environment MAIL_/SMTP_ variables before production.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<?php page_footer(); ?>
|
||||
|
||||
57
lead.php
Normal file
57
lead.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$lead = null;
|
||||
$error = '';
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$lead = fetch_lead_by_id($id);
|
||||
}
|
||||
if (!$lead) {
|
||||
$error = 'Lead not found.';
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$error = 'Lead detail is temporarily unavailable.';
|
||||
error_log('Lead detail failed: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
page_head('Lead Detail — ' . project_name(), 'Detailed view of a captured landing page lead.');
|
||||
page_nav('leads');
|
||||
?>
|
||||
<main class="section">
|
||||
<div class="container">
|
||||
<a class="back-link" href="leads.php">← Back to leads</a>
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-warning mt-3" role="alert"><?= e($error) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="detail-layout mt-3">
|
||||
<article class="detail-card">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-3 mb-4">
|
||||
<div>
|
||||
<p class="eyebrow">Lead #<?= e((string)$lead['id']) ?></p>
|
||||
<h1 class="page-title mb-1"><?= e($lead['name']) ?></h1>
|
||||
<p class="text-muted mb-0"><a href="mailto:<?= e($lead['email']) ?>"><?= e($lead['email']) ?></a></p>
|
||||
</div>
|
||||
<div class="text-md-end">
|
||||
<span class="badge text-bg-light border"><?= e(ucfirst($lead['status'])) ?></span>
|
||||
<?= !empty($lead['email_sent']) ? '<span class="badge text-bg-success">Email sent</span>' : '<span class="badge text-bg-secondary">Email not sent</span>' ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="submitted-summary mb-4">
|
||||
<div><span>Company</span><strong><?= e($lead['company'] ?: '—') ?></strong></div>
|
||||
<div><span>Budget</span><strong><?= e($lead['budget'] ?: 'Not sure yet') ?></strong></div>
|
||||
<div><span>Created</span><strong><?= e(date('M j, Y H:i', strtotime($lead['created_at']))) ?></strong></div>
|
||||
</div>
|
||||
<h2>Message</h2>
|
||||
<p class="message-box"><?= nl2br(e($lead['message'])) ?></p>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a class="btn btn-dark" href="mailto:<?= e($lead['email']) ?>?subject=Re:%20Your%20landing%20page%20request">Reply by email</a>
|
||||
<a class="btn btn-outline-dark" href="index.php#lead-form">Capture another lead</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
<?php page_footer(); ?>
|
||||
60
leads.php
Normal file
60
leads.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$leads = [];
|
||||
$error = '';
|
||||
try {
|
||||
$leads = latest_leads(25);
|
||||
} catch (Throwable $exception) {
|
||||
$error = 'Leads are temporarily unavailable.';
|
||||
error_log('Leads list failed: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
page_head('Lead Inbox — ' . project_name(), 'Review captured landing page leads.');
|
||||
page_nav('leads');
|
||||
?>
|
||||
<main class="section">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end gap-3 mb-4">
|
||||
<div>
|
||||
<p class="eyebrow">Lead inbox</p>
|
||||
<h1 class="page-title">Captured requests</h1>
|
||||
<p class="text-muted mb-0">Newest submissions from the landing page form.</p>
|
||||
</div>
|
||||
<a class="btn btn-dark" href="index.php#lead-form">Add test lead</a>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger" role="alert"><?= e($error) ?></div>
|
||||
<?php elseif (!$leads): ?>
|
||||
<div class="empty-state">
|
||||
<h2>No leads yet</h2>
|
||||
<p>Submit the landing page form to see requests appear here.</p>
|
||||
<a class="btn btn-dark" href="index.php#lead-form">Open form</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-card">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<caption class="visually-hidden">Captured landing page leads</caption>
|
||||
<thead><tr><th>Name</th><th>Company</th><th>Budget</th><th>Status</th><th>Email</th><th>Created</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($leads as $lead): ?>
|
||||
<tr>
|
||||
<td><strong><?= e($lead['name']) ?></strong><br><span class="text-muted small"><?= e(short_text($lead['message'], 54)) ?></span></td>
|
||||
<td><?= e($lead['company'] ?: '—') ?></td>
|
||||
<td><?= e($lead['budget'] ?: 'Not sure') ?></td>
|
||||
<td><span class="badge text-bg-light border"><?= e(ucfirst($lead['status'])) ?></span></td>
|
||||
<td><?= !empty($lead['email_sent']) ? '<span class="badge text-bg-success">Sent</span>' : '<span class="badge text-bg-secondary">Not sent</span>' ?></td>
|
||||
<td><time datetime="<?= e($lead['created_at']) ?>"><?= e(date('M j, Y H:i', strtotime($lead['created_at']))) ?></time></td>
|
||||
<td class="text-end"><a class="btn btn-outline-dark btn-sm" href="lead.php?id=<?= e((string)$lead['id']) ?>">View</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
<?php page_footer(); ?>
|
||||
39
thank-you.php
Normal file
39
thank-you.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$token = isset($_GET['token']) ? preg_replace('/[^a-f0-9]/', '', strtolower((string)$_GET['token'])) : '';
|
||||
$lead = null;
|
||||
if ($token !== '') {
|
||||
try {
|
||||
$lead = fetch_lead_by_token($token);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Thank-you lead lookup failed: ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
page_head('Thank You — ' . project_name(), 'Confirmation page for your landing page request.');
|
||||
page_nav('home');
|
||||
?>
|
||||
<main class="section confirmation-page">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<article class="confirmation-card">
|
||||
<p class="eyebrow">Request submitted</p>
|
||||
<h1>Thank you<?= $lead ? ', ' . e($lead['name']) : '' ?>.</h1>
|
||||
<p class="lead-copy">Your request has been saved. If email notifications are configured, the team also received a message with your details.</p>
|
||||
<?php if ($lead): ?>
|
||||
<div class="submitted-summary">
|
||||
<div><span>Email</span><strong><?= e($lead['email']) ?></strong></div>
|
||||
<div><span>Budget</span><strong><?= e($lead['budget'] ?: 'Not sure yet') ?></strong></div>
|
||||
<div><span>Status</span><strong><?= e(ucfirst($lead['status'])) ?></strong></div>
|
||||
</div>
|
||||
<a class="btn btn-dark" href="lead.php?id=<?= e((string)$lead['id']) ?>">View saved lead</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-dark" href="index.php">Back to landing page</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php page_footer(); ?>
|
||||
Loading…
x
Reference in New Issue
Block a user