diff --git a/app.php b/app.php
new file mode 100644
index 0000000..c71c60a
--- /dev/null
+++ b/app.php
@@ -0,0 +1,455 @@
+exec($sql);
+ $done = true;
+}
+
+function admin_email(): string
+{
+ return app_env('ADMIN_EMAIL', 'admin@local.test');
+}
+
+function admin_password(): string
+{
+ return app_env('ADMIN_PASSWORD', 'admin12345');
+}
+
+function using_default_admin_credentials(): bool
+{
+ return app_env('ADMIN_EMAIL', '') === '' || app_env('ADMIN_PASSWORD', '') === '';
+}
+
+function attempt_admin_login(string $email, string $password): bool
+{
+ if (!hash_equals(strtolower(admin_email()), strtolower(trim($email)))) {
+ return false;
+ }
+
+ if (!hash_equals(admin_password(), $password)) {
+ return false;
+ }
+
+ $_SESSION['admin_logged_in'] = true;
+ $_SESSION['admin_email'] = admin_email();
+ return true;
+}
+
+function logout_admin(): void
+{
+ $_SESSION = [];
+ if (ini_get('session.use_cookies')) {
+ $params = session_get_cookie_params();
+ setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+ }
+ session_destroy();
+}
+
+function is_admin_logged_in(): bool
+{
+ return !empty($_SESSION['admin_logged_in']);
+}
+
+function require_admin(): void
+{
+ if (!is_admin_logged_in()) {
+ set_flash('warning', 'Please sign in to access the dashboard.');
+ header('Location: /login.php');
+ exit;
+ }
+}
+
+function set_flash(string $type, string $message): void
+{
+ $_SESSION['flash'] = ['type' => $type, 'message' => $message];
+}
+
+function get_flash(): ?array
+{
+ if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
+ return null;
+ }
+
+ $flash = $_SESSION['flash'];
+ unset($_SESSION['flash']);
+ return $flash;
+}
+
+function request_status_options(): array
+{
+ return [
+ 'new' => 'New',
+ 'reviewed' => 'Reviewed',
+ 'qualified' => 'Qualified',
+ 'closed' => 'Closed',
+ ];
+}
+
+function request_type_options(): array
+{
+ return [
+ 'Internal tool' => 'Internal tool',
+ 'Admin dashboard' => 'Admin dashboard',
+ 'Client portal' => 'Client portal',
+ 'Marketing site' => 'Marketing site',
+ 'Other MVP' => 'Other MVP',
+ ];
+}
+
+function timeline_options(): array
+{
+ return [
+ 'ASAP' => 'ASAP',
+ '2-4 weeks' => '2–4 weeks',
+ '1-2 months' => '1–2 months',
+ 'Flexible' => 'Flexible',
+ ];
+}
+
+function budget_options(): array
+{
+ return [
+ '<5k' => 'Under $5k',
+ '5k-15k' => '$5k–$15k',
+ '15k-50k' => '$15k–$50k',
+ '50k+' => '$50k+',
+ 'Unknown' => 'Still defining',
+ ];
+}
+
+function validate_request_input(array $input): array
+{
+ $errors = [];
+
+ $name = trim((string) ($input['name'] ?? ''));
+ $email = trim((string) ($input['email'] ?? ''));
+ $company = trim((string) ($input['company'] ?? ''));
+ $projectType = trim((string) ($input['project_type'] ?? ''));
+ $timeline = trim((string) ($input['timeline'] ?? ''));
+ $budget = trim((string) ($input['budget'] ?? ''));
+ $details = trim((string) ($input['details'] ?? ''));
+
+ if ($name === '' || app_strlen($name) < 2) {
+ $errors['name'] = 'Enter a contact name.';
+ }
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ $errors['email'] = 'Enter a valid work email.';
+ }
+ if ($projectType === '' || !array_key_exists($projectType, request_type_options())) {
+ $errors['project_type'] = 'Choose the type of MVP you need.';
+ }
+ if ($timeline === '' || !array_key_exists($timeline, timeline_options())) {
+ $errors['timeline'] = 'Choose a timeline.';
+ }
+ if ($budget === '' || !array_key_exists($budget, budget_options())) {
+ $errors['budget'] = 'Choose a budget range.';
+ }
+ if ($details === '' || app_strlen($details) < 20) {
+ $errors['details'] = 'Share at least a short brief (20+ characters).';
+ }
+
+ return [
+ 'errors' => $errors,
+ 'data' => [
+ 'name' => app_substr($name, 0, 120),
+ 'email' => app_substr($email, 0, 190),
+ 'company' => app_substr($company, 0, 160),
+ 'project_type' => $projectType,
+ 'timeline' => $timeline,
+ 'budget' => $budget,
+ 'details' => app_substr($details, 0, 4000),
+ ],
+ ];
+}
+
+function create_request(array $input): int
+{
+ ensure_schema();
+ $validated = validate_request_input($input);
+ if ($validated['errors']) {
+ throw new InvalidArgumentException('Request data is invalid.');
+ }
+
+ $data = $validated['data'];
+ $stmt = db()->prepare('INSERT INTO project_requests (name, email, company, project_type, timeline, budget, details, status, created_at) VALUES (:name, :email, :company, :project_type, :timeline, :budget, :details, :status, UTC_TIMESTAMP())');
+ $stmt->bindValue(':name', $data['name']);
+ $stmt->bindValue(':email', $data['email']);
+ $stmt->bindValue(':company', $data['company'] === '' ? null : $data['company'], $data['company'] === '' ? PDO::PARAM_NULL : PDO::PARAM_STR);
+ $stmt->bindValue(':project_type', $data['project_type']);
+ $stmt->bindValue(':timeline', $data['timeline']);
+ $stmt->bindValue(':budget', $data['budget']);
+ $stmt->bindValue(':details', $data['details']);
+ $stmt->bindValue(':status', 'new');
+ $stmt->execute();
+
+ return (int) db()->lastInsertId();
+}
+
+function fetch_requests(?string $status = null): array
+{
+ ensure_schema();
+ if ($status !== null && $status !== '' && array_key_exists($status, request_status_options())) {
+ $stmt = db()->prepare('SELECT * FROM project_requests WHERE status = :status ORDER BY created_at DESC, id DESC');
+ $stmt->bindValue(':status', $status);
+ $stmt->execute();
+ return $stmt->fetchAll();
+ }
+
+ $stmt = db()->query('SELECT * FROM project_requests ORDER BY created_at DESC, id DESC');
+ return $stmt->fetchAll();
+}
+
+function fetch_request(int $id): ?array
+{
+ ensure_schema();
+ $stmt = db()->prepare('SELECT * FROM project_requests WHERE id = :id LIMIT 1');
+ $stmt->bindValue(':id', $id, PDO::PARAM_INT);
+ $stmt->execute();
+ $request = $stmt->fetch();
+
+ return $request ?: null;
+}
+
+function update_request_status(int $id, string $status): bool
+{
+ ensure_schema();
+ if (!array_key_exists($status, request_status_options())) {
+ return false;
+ }
+
+ $stmt = db()->prepare('UPDATE project_requests SET status = :status WHERE id = :id');
+ $stmt->bindValue(':status', $status);
+ $stmt->bindValue(':id', $id, PDO::PARAM_INT);
+ return $stmt->execute();
+}
+
+function dashboard_metrics(): array
+{
+ ensure_schema();
+ $rows = db()->query('SELECT status, COUNT(*) AS count_rows FROM project_requests GROUP BY status')->fetchAll();
+ $metrics = [
+ 'total' => 0,
+ 'new' => 0,
+ 'reviewed' => 0,
+ 'qualified' => 0,
+ 'closed' => 0,
+ ];
+
+ foreach ($rows as $row) {
+ $status = (string) $row['status'];
+ $count = (int) $row['count_rows'];
+ $metrics['total'] += $count;
+ if (array_key_exists($status, $metrics)) {
+ $metrics[$status] = $count;
+ }
+ }
+
+ return $metrics;
+}
+
+function request_reference(int $id): string
+{
+ return 'REQ-' . str_pad((string) $id, 4, '0', STR_PAD_LEFT);
+}
+
+function format_datetime(string $value): string
+{
+ try {
+ $date = new DateTimeImmutable($value, new DateTimeZone('UTC'));
+ return $date->format('M j, Y \a\t H:i') . ' UTC';
+ } catch (Throwable $e) {
+ return $value;
+ }
+}
+
+function status_badge_class(string $status): string
+{
+ return match ($status) {
+ 'new' => 'text-bg-primary',
+ 'reviewed' => 'text-bg-secondary',
+ 'qualified' => 'text-bg-success',
+ 'closed' => 'text-bg-dark',
+ default => 'text-bg-light',
+ };
+}
+
+function active_nav(string $active, string $value): string
+{
+ return $active === $value ? 'active' : '';
+}
+
+function render_head(string $title, string $pageDescription = '', bool $noindex = false): void
+{
+ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: '';
+ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? getenv('PROJECT_IMAGE_URL') ?: '';
+ $description = $pageDescription !== '' ? $pageDescription : ($projectDescription ?: project_description());
+ $assetVersion = (string) (filemtime(__DIR__ . '/assets/css/custom.css') ?: time());
+ ?>
+
+
+
+
+
+ = e($title) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'text-bg-success',
+ 'danger' => 'text-bg-danger',
+ 'warning' => 'text-bg-warning',
+ 'info' => 'text-bg-primary',
+ ];
+ $class = $map[$type] ?? 'text-bg-primary';
+ ?>
+
+
+
+
= e((string) ($flash['message'] ?? 'Done.')) ?>
+
+
+
+
+
+
+
+
+
+
+ {
- 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');
+ document.querySelectorAll('.toast').forEach((toastEl) => {
+ if (window.bootstrap) {
+ const toast = new bootstrap.Toast(toastEl, { delay: 3500 });
+ toast.show();
}
});
+
+ document.querySelectorAll('[data-copy-text]').forEach((button) => {
+ button.addEventListener('click', async () => {
+ const text = button.getAttribute('data-copy-text') || '';
+ try {
+ await navigator.clipboard.writeText(text);
+ button.textContent = 'Copied';
+ window.setTimeout(() => {
+ button.textContent = 'Copy demo credentials';
+ }, 1600);
+ } catch (error) {
+ console.error('Clipboard copy failed', error);
+ }
+ });
+ });
+
+ const hash = window.location.hash;
+ if (hash && hash.length > 1) {
+ const target = document.querySelector(hash);
+ if (target) {
+ window.setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 120);
+ }
+ }
});
diff --git a/dashboard.php b/dashboard.php
new file mode 100644
index 0000000..c0f817c
--- /dev/null
+++ b/dashboard.php
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
Dashboard
+
Project request queue
+
Review the latest submissions, filter by status, and open each request detail page.
+
+
Create another request
+
+
+
+
Total requests
= e((string) $metrics['total']) ?>
+
+
+
New
= e((string) $metrics['new']) ?>
+
+
+
Reviewed
= e((string) $metrics['reviewed']) ?>
+
+
+
Qualified
= e((string) $metrics['qualified']) ?>
+
+
+
+
+
+
+
+
+
+
All requests
+
Thin-slice admin table with filters and drill-down details.
+
+
+
+
+
+
+
No requests yet
+
Submit the first project request from the home page to populate the queue.
+
Create the first request
+
+
+
+
+
+
+ Reference
+ Contact
+ Type
+ Timeline
+ Status
+ Submitted
+ Action
+
+
+
+
+
+ = e(request_reference((int) $request['id'])) ?>
+
+ = e($request['name']) ?>
+ = e($request['email']) ?>
+
+ = e($request['project_type']) ?>
+ = e(timeline_options()[$request['timeline']] ?? $request['timeline']) ?>
+ = e(request_status_options()[$request['status']] ?? ucfirst((string) $request['status'])) ?>
+ = e(format_datetime((string) $request['created_at'])) ?>
+ Open
+
+
+
+
+
+
+
+
+
+
+
diff --git a/db/migrations/001_project_requests.sql b/db/migrations/001_project_requests.sql
new file mode 100644
index 0000000..1b4fa2a
--- /dev/null
+++ b/db/migrations/001_project_requests.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS project_requests (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ name VARCHAR(120) NOT NULL,
+ email VARCHAR(190) NOT NULL,
+ company VARCHAR(160) NULL,
+ project_type VARCHAR(120) NOT NULL,
+ timeline VARCHAR(80) NOT NULL,
+ budget VARCHAR(80) NOT NULL,
+ details TEXT NOT NULL,
+ status VARCHAR(30) NOT NULL DEFAULT 'new',
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY idx_project_requests_status (status),
+ KEY idx_project_requests_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/index.php b/index.php
index 7205f3d..b0014d8 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,222 @@
'',
+ 'email' => '',
+ 'company' => '',
+ 'project_type' => 'Admin dashboard',
+ 'timeline' => '2-4 weeks',
+ 'budget' => '5k-15k',
+ 'details' => '',
+];
+$errors = [];
+$createdId = isset($_GET['ref']) ? (int) $_GET['ref'] : 0;
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $validation = validate_request_input($_POST);
+ $formData = array_merge($formData, $validation['data']);
+ $errors = $validation['errors'];
+
+ if (!$errors) {
+ $requestId = create_request($formData);
+ set_flash('success', 'Request submitted. You can now review it in the admin dashboard.');
+ header('Location: /?submitted=1&ref=' . $requestId . '#request-form');
+ exit;
+ }
+}
+
+render_head(project_name() . ' | Intake & admin dashboard', 'Submit a new project request and review it in a clean admin dashboard.');
+render_navbar('home');
+render_flash_toast();
?>
-
-
-
-
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Analyzing your requirements and generating your website…
-
-
Loading…
+
+
+
+
+
+
Fast LAMP MVP · intake to dashboard
+
A focused admin app starter for capturing and reviewing project requests.
+
This first slice gives you a public intake form, a secure admin sign-in, a dashboard list, and a detail screen so the workflow already feels like a usable product.
+
+
+
+
+
1 form
+
public intake path
+
+
+
+
+
1 dashboard
+
review queue
+
+
+
+
+
1 detail view
+
status updates
+
+
+
+
+
+
+
+
+
Admin access
+
Demo sign-in
+
Use the admin login to review submissions right away.
+
+
Ready
+
+
+
+ Email
+ = e(admin_email()) ?>
+
+
+ Password
+ = e(admin_password()) ?>
+
+
+
+
+
For this MVP, fallback credentials are enabled. Replace ADMIN_EMAIL and ADMIN_PASSWORD in the environment when you are ready.
+
+
+
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
-
-
-
+
+
+
+
+
+
+
+
Step 1
+
Capture the brief
+
Collect the essentials: contact, project type, budget, timeline, and a concise brief.
+
+
+
+
+
Step 2
+
Review in dashboard
+
Admins get a clean queue view with filters, counts, and a direct path to request details.
+
+
+
+
+
Step 3
+
Update status
+
Each request has a detail page where you can move it from new to reviewed, qualified, or closed.
+
+
+
+
+
+
+
+
+
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..130404d
--- /dev/null
+++ b/login.php
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
Secure area
+
Admin login
+
Sign in to review new requests, open details, and update statuses.
+
+
= e($error) ?>
+
+
+
+ Email
+
+
+
+ Password
+
+
+ Sign in
+
+
+
Demo credentials
+
Email = e(admin_email()) ?>
+
Password = e(admin_password()) ?>
+
+
+
+
+
+
+
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..b49a8e2
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,11 @@
+ 0 ? fetch_request($id) : null;
+
+if (!$request) {
+ http_response_code(404);
+ render_head(project_name() . ' | Request not found', 'Requested submission was not found.', true);
+ render_navbar('detail');
+ ?>
+
Request not found The requested submission does not exist or was removed.
Back to dashboard
+
+
+
+
+
+
Request detail
+
= e(request_reference((int) $request['id'])) ?>
+
Submitted = e(format_datetime((string) $request['created_at'])) ?>
+
+
+
Back to dashboard
+
= e(request_status_options()[$request['status']] ?? ucfirst((string) $request['status'])) ?>
+
+
+
+
+
+
+
+
+
Contact
+
= e($request['name']) ?>
+
+
+
Email
+
= e($request['email']) ?>
+
+
+
Company
+
= e($request['company'] ?: 'Not provided') ?>
+
+
+
Project type
+
= e($request['project_type']) ?>
+
+
+
Timeline
+
= e(timeline_options()[$request['timeline']] ?? $request['timeline']) ?>
+
+
+
Budget
+
= e(budget_options()[$request['budget']] ?? $request['budget']) ?>
+
+
+
Project brief
+
= nl2br(e((string) $request['details'])) ?>
+
+
+
+
+
+
+
Status
+
Update request stage
+
+
+ Current stage
+
+ $label): ?>
+ >= e($label) ?>
+
+
+
+ Save status
+
+
+ Use this page as the handoff surface between intake and next-step qualification. Edit/delete can be added later if needed.
+
+
+
+
+
+
+