Compare commits

...

2 Commits

Author SHA1 Message Date
Flatlogic Bot
011d28fa8c seck 2025-11-13 18:26:31 +00:00
Flatlogic Bot
df3eb075fa initial 2025-11-13 17:54:09 +00:00
28 changed files with 1226 additions and 147 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
node_modules/ node_modules/
*/node_modules/ */node_modules/
*/build/ */build/
# Composer
vendor/
composer.lock
composer.phar

92
analyze.php Normal file
View File

@ -0,0 +1,92 @@
<?php
session_start();
require_once 'db/config.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit();
}
// Check for sufficient credits
$pdo = db();
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if (!$user || $user['credits'] <= 0) {
// Redirect or show an error if credits are insufficient
$_SESSION['error_message'] = 'You have no credits left. Please purchase more to continue.';
header('Location: pricing.php');
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_id'])) {
$uploadId = $_POST['upload_id'];
$userId = $_SESSION['user_id'];
// Re-verify the upload belongs to the user
$stmt = $pdo->prepare("SELECT * FROM uploads WHERE id = ? AND user_id = ?");
$stmt->execute([$uploadId, $userId]);
$upload = $stmt->fetch();
if ($upload) {
// Deduct one credit BEFORE starting the analysis
$pdo->prepare("UPDATE users SET credits = credits - 1 WHERE id = ?")->execute([$userId]);
// Update status to 'analyzing'
$updateStmt = $pdo->prepare("UPDATE uploads SET status = 'analyzing' WHERE id = ?");
$updateStmt->execute([$uploadId]);
// --- Real CV Service Integration ---
$bearerToken = getenv('INTERNAL_CV_BEARER_TOKEN');
$cvServiceUrl = 'https://internal-model/analyze';
$analysisResult = null;
$newStatus = 'failed';
if (!$bearerToken) {
$analysisResult = ['error' => 'Internal server configuration error: CV service token not set.'];
} elseif (empty($upload['file_path']) || !file_exists($upload['file_path'])) {
$analysisResult = ['error' => 'File not found for analysis.'];
} else {
// Prepare cURL request
$ch = curl_init();
$cfile = new CURLFile($upload['file_path'], mime_content_type($upload['file_path']), basename($upload['file_path']));
curl_setopt($ch, CURLOPT_URL, $cvServiceUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['image' => $cfile]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $bearerToken,
'Accept: application/json',
]);
// IMPORTANT: In a real production environment, you would not disable SSL verification.
// This is included for local/dev environments with self-signed certificates.
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
$analysisResult = ['error' => 'cURL Error: ' . $curlError];
} elseif ($httpCode >= 200 && $httpCode < 300) {
$analysisResult = json_decode($response, true);
$newStatus = 'completed';
} else {
$analysisResult = ['error' => 'CV service returned HTTP ' . $httpCode, 'response' => $response];
}
}
// Store the result and update status
$resultStmt = $pdo->prepare("UPDATE uploads SET status = ?, analysis_result = ? WHERE id = ?");
$resultStmt->execute([$newStatus, json_encode($analysisResult), $uploadId]);
}
}
header('Location: index.php');
exit();
?>

139
assets/css/custom.css Normal file
View File

@ -0,0 +1,139 @@
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #F8F9FA;
}
.header-gradient {
background: linear-gradient(135deg, #007BFF, #0056b3);
}
.upload-zone {
border: 2px dashed #007BFF;
border-radius: 0.5rem;
padding: 4rem;
text-align: center;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.upload-zone.drag-over {
background-color: #e9ecef;
}
.upload-zone .icon {
width: 48px;
height: 48px;
stroke-width: 1.5;
margin-bottom: 1rem;
}
.result-card {
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Auth Forms */
.auth-form {
max-width: 400px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.auth-form h1 {
text-align: center;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.5;
color: #fff;
text-align: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
background-color: #007bff;
border: 1px solid #007bff;
padding: 0.75rem 1.25rem;
font-size: 1rem;
border-radius: 0.25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
width: 100%;
}
.btn:hover {
background-color: #0069d9;
border-color: #0062cc;
}
.alert {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.upload-card {
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s ease-in-out;
}
.upload-card:hover {
transform: translateY(-5px);
}
.upload-card .card-img-top {
aspect-ratio: 16 / 9;
object-fit: cover;
}
.analysis-result {
background-color: #f8f9fa;
border-left: 3px solid #007BFF;
padding: 0.5rem;
margin-top: 0.5rem;
font-size: 0.875rem;
border-radius: 0.25rem;
}
.bounding-box {
position: absolute;
border: 2px solid #ff4d4d;
background-color: rgba(255, 77, 77, 0.2);
box-shadow: 0 0 5px rgba(255, 77, 77, 0.5);
pointer-events: none; /* So it doesn't interfere with image interactions */
}

29
assets/js/main.js Normal file
View File

@ -0,0 +1,29 @@
document.addEventListener('DOMContentLoaded', () => {
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('vehicleImage');
const uploadForm = document.getElementById('uploadForm');
if (uploadZone) {
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('drag-over');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('drag-over');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('drag-over');
if (e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
// Automatically submit the form when a file is dropped
uploadForm.submit();
}
});
}
});

81
billing.php Normal file
View File

@ -0,0 +1,81 @@
<?php
require_once __DIR__ . '/partials/header.php';
require_once __DIR__ . '/db/config.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$userId = $_SESSION['user_id'];
$pdo = db();
// Fetch user's current credit balance
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
$user_credits = $user ? $user['credits'] : 0;
// Fetch purchase history
$stmt = $pdo->prepare(
"SELECT p.credits_purchased, p.amount_paid, p.created_at, pl.name as plan_name " .
"FROM purchases p " .
"JOIN plans pl ON p.plan_id = pl.id " .
"WHERE p.user_id = ? ORDER BY p.created_at DESC"
);
$stmt->execute([$userId]);
$purchases = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container">
<div class="text-center my-5">
<h1>Billing & Credits</h1>
<p class="lead">View your credit balance and purchase history.</p>
</div>
<div class="row">
<div class="col-md-4">
<div class="card shadow-sm mb-4">
<div class="card-body text-center">
<h5 class="card-title">Your Credits</h5>
<p class="display-4 fw-bold"><?= $user_credits ?></p>
<a href="pricing.php" class="btn btn-primary">Buy More Credits</a>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">Purchase History</h5>
<?php if ($purchases): ?>
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Package</th>
<th>Credits</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ($purchases as $purchase): ?>
<tr>
<td><?= date('M d, Y', strtotime($purchase['created_at'])) ?></td>
<td><?= htmlspecialchars($purchase['plan_name']) ?></td>
<td>+<?= htmlspecialchars($purchase['credits_purchased']) ?></td>
<td>$<?= htmlspecialchars(number_format($purchase['amount_paid'], 2)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>You have not made any purchases yet.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/partials/footer.php'; ?>

40
checkout.php Normal file
View File

@ -0,0 +1,40 @@
<?php
require_once 'vendor/autoload.php';
require_once 'db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$stripe_secret_key = getenv('STRIPE_SECRET_KEY');
if (!$stripe_secret_key) {
die('Stripe secret key is not configured.');
}
\Stripe\Stripe::setApiKey($stripe_secret_key);
$price_id = $_GET['price_id'] ?? null;
if (!$price_id) {
header('Location: pricing.php');
exit;
}
$user_email = $_SESSION['user_email']; // Assuming user_email is stored in session from login
$checkout_session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price' => $price_id,
'quantity' => 1,
]],
'mode' => 'subscription',
'success_url' => 'http://' . $_SERVER['HTTP_HOST'] . '/index.php?payment=success',
'cancel_url' => 'http://' . $_SERVER['HTTP_HOST'] . '/pricing.php?payment=cancel',
'customer_email' => $user_email,
'client_reference_id' => $_SESSION['user_id']
]);
header("Location: " . $checkout_session->url);

13
composer.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "flatlogic/vda",
"description": "Vehicle Damage Analysis",
"authors": [
{
"name": "Flatlogic Bot",
"email": "support@flatlogic.com"
}
],
"require": {
"stripe/stripe-php": "^18.2"
}
}

57
create-portal-session.php Normal file
View File

@ -0,0 +1,57 @@
<?php
require_once 'vendor/autoload.php';
require_once 'db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Load Stripe API key from .env
$stripeSecretKey = getenv('STRIPE_SECRET_KEY');
if (!$stripeSecretKey) {
die('Stripe secret key is not configured.');
}
\Stripe\Stripe::setApiKey($stripeSecretKey);
// Get the user's Stripe Customer ID from your database
$userId = $_SESSION['user_id'];
$customerId = null;
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT stripe_customer_id FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC LIMIT 1");
$stmt->execute([$userId]);
$customerId = $stmt->fetchColumn();
} catch (PDOException $e) {
die('Could not retrieve customer data.');
}
if (!$customerId) {
// This can happen if the subscription was created but the webhook failed.
// Or if the user has no subscription.
header('Location: billing.php?error=nocustomer');
exit;
}
// The return URL to which the user will be redirected after managing their billing
$returnUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . '/billing.php';
try {
// Create a Billing Portal session
$portalSession = \Stripe\BillingPortal\Session::create([
'customer' => $customerId,
'return_url' => $returnUrl,
]);
// Redirect to the session URL
header("Location: " . $portalSession->url);
exit();
} catch (\Stripe\Exception\ApiErrorException $e) {
// Handle Stripe API errors
// You might want to log this error and show a generic message
die('Stripe API error: ' . $e->getMessage());
}

View File

@ -15,3 +15,16 @@ function db() {
} }
return $pdo; return $pdo;
} }
function hasActiveSubscription($user_id) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id FROM subscriptions WHERE user_id = ? AND status = 'active' AND current_period_end > NOW()");
$stmt->execute([$user_id]);
return $stmt->fetch() !== false;
} catch (PDOException $e) {
// Log error if needed
error_log('Subscription check failed: ' . $e->getMessage());
return false;
}
}

49
db/migrate.php Normal file
View File

@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
// 1. Create migrations table if it doesn't exist
$pdo->exec("CREATE TABLE IF NOT EXISTS `migrations` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `migration` VARCHAR(255) NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP );");
// 2. Get all executed migrations
$stmt = $pdo->query("SELECT `migration` FROM `migrations`");
$executed_migrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 3. Find and execute new migrations
$migration_files = glob(__DIR__ . '/migrations/*.sql');
sort($migration_files);
$new_migrations_found = false;
foreach ($migration_files as $migration_file) {
$migration_name = basename($migration_file);
if (!in_array($migration_name, $executed_migrations)) {
$new_migrations_found = true;
$sql = file_get_contents($migration_file);
try {
$pdo->exec($sql);
// 4. Log the new migration
$insert_stmt = $pdo->prepare("INSERT INTO `migrations` (`migration`) VALUES (?)");
$insert_stmt->execute([$migration_name]);
echo "Executed migration: " . $migration_name . PHP_EOL;
} catch (PDOException $e) {
// If a specific migration fails, output the error and stop.
die("Migration failed on " . $migration_name . ": " . $e->getMessage() . PHP_EOL);
}
}
}
if (!$new_migrations_found) {
echo "No new migrations to execute." . PHP_EOL;
} else {
echo "All new migrations executed successfully." . PHP_EOL;
}
} catch (PDOException $e) {
die("Database operation failed: " . $e->getMessage() . PHP_EOL);
}

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `users` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(255) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS `uploads` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT(11) UNSIGNED NOT NULL,
`file_path` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,3 @@
ALTER TABLE `uploads`
ADD COLUMN `status` VARCHAR(50) NOT NULL DEFAULT 'pending',
ADD COLUMN `analysis_result` TEXT DEFAULT NULL;

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `plans` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`stripe_price_id` VARCHAR(255) NOT NULL,
`price` DECIMAL(10, 2) NOT NULL,
`features` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `subscriptions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT(11) UNSIGNED NOT NULL,
`plan_id` INT NOT NULL,
`stripe_subscription_id` VARCHAR(255) NOT NULL,
`status` VARCHAR(50) NOT NULL,
`current_period_end` TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`)
);

View File

@ -0,0 +1,2 @@
ALTER TABLE `users` ADD `credits` INT UNSIGNED NOT NULL DEFAULT 0;
ALTER TABLE `plans` ADD `credits_awarded` INT UNSIGNED NOT NULL DEFAULT 0;

View File

@ -0,0 +1,15 @@
-- Drop the now-obsolete subscriptions table
DROP TABLE IF EXISTS `subscriptions`;
-- Create a table to log credit purchases
CREATE TABLE IF NOT EXISTS `purchases` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT(11) UNSIGNED NOT NULL,
`plan_id` INT NOT NULL,
`stripe_charge_id` VARCHAR(255) NOT NULL,
`credits_purchased` INT(11) NOT NULL,
`amount_paid` DECIMAL(10, 2) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

31
db/seed_plans.php Normal file
View File

@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
// Check if plans already exist
$stmt = $pdo->query("SELECT COUNT(*) FROM plans");
if ($stmt->fetchColumn() > 0) {
echo "Plans table is already seeded." . PHP_EOL;
exit;
}
// Insert the "Starter" credit pack
// IMPORTANT: Replace 'price_12345' with your actual Stripe Price ID for the credit pack
$starterPack = [
'name' => 'Starter Pack',
'stripe_price_id' => 'price_1PeP3QRpH4kRz8A8e25a25fA', // Placeholder - REPLACE THIS
'price' => 5.00,
'credits_awarded' => 50,
'features' => json_encode(['50 analysis credits', 'Standard support']),
];
$sql = "INSERT INTO plans (name, stripe_price_id, price, credits_awarded, features) VALUES (:name, :stripe_price_id, :price, :credits_awarded, :features)";
$stmt = $pdo->prepare($sql);
$stmt->execute($starterPack);
echo "Successfully seeded the 'plans' table with the Starter Pack." . PHP_EOL;
} catch (PDOException $e) {
die("Database error: " . $e->getMessage());
}

333
index.php
View File

@ -1,150 +1,195 @@
<?php <?php
declare(strict_types=1); require_once 'partials/header.php';
@ini_set('display_errors', '1'); require_once 'db/config.php';
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // If the user is not logged in, redirect to the login page.
$now = date('Y-m-d H:i:s'); if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$pdo = db();
$stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$user_credits = $user ? $user['credits'] : 0;
$upload_dir = 'uploads/';
$uploaded_file_path = null;
$error_message = null;
if ($user_credits > 0 && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['vehicleImage'])) {
if ($_FILES['vehicleImage']['error'] === UPLOAD_ERR_OK) {
$tmp_name = $_FILES['vehicleImage']['tmp_name'];
$name = basename($_FILES['vehicleImage']['name']);
$file_ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$allowed_ext = ['jpg', 'jpeg', 'png', 'gif'];
if (in_array($file_ext, $allowed_ext)) {
// Create a unique filename to avoid conflicts
$new_filename = uniqid('', true) . '.' . $file_ext;
$destination = $upload_dir . $new_filename;
if (move_uploaded_file($tmp_name, $destination)) {
$uploaded_file_path = $destination;
try {
$stmt = db()->prepare("INSERT INTO uploads (user_id, file_path) VALUES (?, ?)");
$stmt->execute([$_SESSION['user_id'], $uploaded_file_path]);
} catch (PDOException $e) {
$error_message = "Database error: " . $e->getMessage();
// Optionally, delete the uploaded file if DB insertion fails
unlink($destination);
}
} else {
$error_message = 'Failed to move uploaded file.';
}
} else {
$error_message = 'Invalid file type. Please upload a JPG, PNG, or GIF image.';
}
} else {
$error_message = 'File upload failed with error code: ' . $_FILES['vehicleImage']['error'];
}
}
?> ?>
<!doctype html>
<html lang="en">
<head> <main class="container">
<meta charset="utf-8" /> <div class="d-flex justify-content-between align-items-center mb-4">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <h1 class="h2">My Dashboard</h1>
<title>New Style</title> <div class="text-end">
<?php <span class="badge bg-primary fs-6">Credits: <?= $user_credits ?></span>
// Read project preview data from environment <a href="pricing.php" class="btn btn-sm btn-outline-primary ms-2">Buy More</a>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; </div>
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; </div>
?>
<?php if ($projectDescription): ?> <?php if ($user_credits > 0): ?>
<!-- Meta description --> <div class="card p-4 p-md-5 border-0 shadow-sm mb-5">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <div class="row align-items-center">
<!-- Open Graph meta tags --> <div class="col-lg-6 mb-4 mb-lg-0">
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <h2 class="h1 mb-3">Upload Vehicle Photo</h2>
<!-- Twitter meta tags --> <p class="lead mb-4">Get an instant AI-powered damage analysis. Drag and drop an image or click to select a file.</p>
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <form id="uploadForm" action="index.php" method="post" enctype="multipart/form-data">
<?php endif; ?> <div id="uploadZone" class="upload-zone">
<?php if ($projectImageUrl): ?> <i data-feather="upload-cloud" class="icon text-primary"></i>
<!-- Open Graph image --> <p class="m-0"><strong>Drag & drop</strong> or <strong>click to browse</strong></p>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" /> <p class="text-muted small">Supports: JPG, PNG, GIF</p>
<!-- Twitter image --> </div>
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" /> <input type="file" name="vehicleImage" id="vehicleImage" class="d-none">
<?php endif; ?> <button type="submit" class="btn btn-primary btn-lg mt-3">Upload Image</button>
<link rel="preconnect" href="https://fonts.googleapis.com"> </form>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> </div>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <div class="col-lg-6">
<style> <?php if ($error_message): ?>
:root { <div class="alert alert-danger">
--bg-color-start: #6a11cb; <?php echo htmlspecialchars($error_message); ?>
--bg-color-end: #2575fc; </div>
--text-color: #ffffff; <?php endif; ?>
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1); <?php if ($uploaded_file_path): ?>
} <div class="text-center">
body { <h3 class="h5 mb-3">Analysis Result</h3>
margin: 0; <div class="card result-card">
font-family: 'Inter', sans-serif; <img src="<?php echo htmlspecialchars($uploaded_file_path); ?>" class="card-img-top" alt="Uploaded Vehicle Image">
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); <div class="card-body">
color: var(--text-color); <h5 class="card-title">Image Received</h5>
display: flex; <p class="card-text d-flex align-items-center justify-content-center">
justify-content: center; Status: <span class="badge bg-info text-dark ms-2">Pending Analysis</span>
align-items: center; </p>
min-height: 100vh; </div>
text-align: center; </div>
overflow: hidden; </div>
position: relative; <?php endif; ?>
} </div>
body::before { </div>
content: ''; </div>
position: absolute; <?php else: ?>
top: 0; <div class="card p-4 p-md-5 border-0 shadow-sm mb-5 text-center">
left: 0; <h2 class="h1 mb-3">You're Out of Credits!</h2>
width: 100%; <p class="lead mb-4">You need to buy more credits to upload and analyze images.</p>
height: 100%; <a href="pricing.php" class="btn btn-primary btn-lg">Buy Credits</a>
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>'); </div>
animation: bg-pan 20s linear infinite; <?php endif; ?>
z-index: -1;
} <div class="dashboard">
@keyframes bg-pan { <h2 class="h3 mb-4">My Uploads</h2>
0% { background-position: 0% 0%; } <div class="row">
100% { background-position: 100% 100%; } <?php
} $stmt = db()->prepare("SELECT * FROM uploads WHERE user_id = ? ORDER BY created_at DESC");
main { $stmt->execute([$_SESSION['user_id']]);
padding: 2rem; $uploads = $stmt->fetchAll();
}
.card { if (count($uploads) > 0):
background: var(--card-bg-color); foreach ($uploads as $upload):
border: 1px solid var(--card-border-color); $status = htmlspecialchars($upload['status']);
border-radius: 16px; $status_badge_class = 'bg-secondary';
padding: 2rem; if ($status === 'pending') {
backdrop-filter: blur(20px); $status_badge_class = 'bg-warning text-dark';
-webkit-backdrop-filter: blur(20px); } elseif ($status === 'analyzing') {
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1); $status_badge_class = 'bg-info text-dark';
} } elseif ($status === 'completed') {
.loader { $status_badge_class = 'bg-success';
margin: 1.25rem auto 1.25rem; } elseif ($status === 'failed') {
width: 48px; $status_badge_class = 'bg-danger';
height: 48px; }
border: 3px solid rgba(255, 255, 255, 0.25); ?>
border-top-color: #fff; <div class="col-md-4 mb-4">
border-radius: 50%; <div class="card upload-card h-100">
animation: spin 1s linear infinite; <img src="<?php echo htmlspecialchars($upload['file_path']); ?>" class="card-img-top" alt="Uploaded Image">
} <div class="card-body d-flex flex-column">
@keyframes spin { <p class="card-text small text-muted mb-2">
from { transform: rotate(0deg); } Uploaded: <?php echo date("M d, Y", strtotime($upload['created_at'])); ?>
to { transform: rotate(360deg); } </p>
} <p class="card-text mb-3">
.hint { Status: <span class="badge <?php echo $status_badge_class; ?>"><?php echo ucfirst($status); ?></span>
opacity: 0.9; </p>
}
.sr-only { <?php
position: absolute; $result = $upload['analysis_result'] ? json_decode($upload['analysis_result'], true) : null;
width: 1px; height: 1px;
padding: 0; margin: -1px; if ($status === 'completed' && $result && !isset($result['error'])):
overflow: hidden; ?>
clip: rect(0, 0, 0, 0); <div class="analysis-result small mb-3">
white-space: nowrap; border: 0; <strong>Analysis Result:</strong><br>
} Damage Detected: <?php echo isset($result['damage_detected']) && $result['damage_detected'] ? 'Yes' : 'No'; ?><br>
h1 { Confidence: <?php echo $result['confidence'] ?? 'N/A'; ?>%<br>
font-size: 3rem; </div>
font-weight: 700; <?php elseif ($status === 'failed' && $result && isset($result['error'])):
margin: 0 0 1rem; ?>
letter-spacing: -1px; <div class="analysis-result small text-danger mb-3">
} <strong>Analysis Failed:</strong><br>
p { <?php echo htmlspecialchars($result['error']); ?>
margin: 0.5rem 0; </div>
font-size: 1.1rem; <?php endif; ?>
}
code { <div class="mt-auto">
background: rgba(0,0,0,0.2); <?php if ($user_credits > 0): ?>
padding: 2px 6px; <?php if ($status === 'pending' || $status === 'failed'): ?>
border-radius: 4px; <form action="analyze.php" method="post" class="d-grid">
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; <input type="hidden" name="upload_id" value="<?php echo $upload['id']; ?>">
} <button type="submit" class="btn btn-primary btn-sm"><?php echo ($status === 'failed') ? 'Retry Analysis' : 'Analyze'; ?></button>
footer { </form>
position: absolute; <?php elseif ($status === 'analyzing'): ?>
bottom: 1rem; <button type="button" class="btn btn-secondary btn-sm w-100" disabled>Analyzing...</button>
font-size: 0.8rem; <?php elseif ($status === 'completed'): ?>
opacity: 0.7; <a href="report.php?id=<?php echo $upload['id']; ?>" class="btn btn-outline-secondary btn-sm">View Report</a>
} <?php endif; ?>
</style> <?php else: ?>
</head> <a href="pricing.php" class="btn btn-primary btn-sm disabled" title="Buy more credits">Analyze</a>
<body> <?php endif; ?>
<main> </div>
<div class="card"> </div>
<h1>Analyzing your requirements and generating your website…</h1> </div>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> </div>
<span class="sr-only">Loading…</span> <?php
endforeach;
else:
?>
<div class="col">
<p>You haven't uploaded any images yet.</p>
</div>
<?php endif; ?>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <?php require_once 'partials/footer.php'; ?>
</footer>
</body>
</html>

70
login.php Normal file
View File

@ -0,0 +1,70 @@
<?php
require_once 'db/config.php';
require_once 'partials/header.php';
// If user is already logged in, redirect to home
if (isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
$errors[] = 'Email and password are required.';
} else {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_email'] = $user['email'];
header('Location: index.php');
exit;
} else {
$errors[] = 'Invalid email or password.';
}
} catch (PDOException $e) {
$errors[] = 'Database error: ' . $e->getMessage();
}
}
}
?>
<div class="auth-form">
<h1>Login</h1>
<?php if (!empty($errors)):
?>
<div class="alert alert-danger">
<?php foreach ($errors as $error):
?>
<p><?php echo htmlspecialchars($error); ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form action="login.php" method="post">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Login</button>
</form>
<p>Don't have an account? <a href="register.php">Register here</a>.</p>
</div>
<?php
require_once 'partials/footer.php';
?>

6
logout.php Normal file
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_unset();
session_destroy();
header('Location: login.php');
exit;

5
partials/footer.php Normal file
View File

@ -0,0 +1,5 @@
</div> <!-- /container -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>

48
partials/header.php Normal file
View File

@ -0,0 +1,48 @@
<?php
session_start();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vehicle Damage Analysis</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="index.php">Vehicle Damage AI</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="index.php">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="pricing.php">Pricing</a>
</li>
<?php if (isset($_SESSION['user_id'])): ?>
<li class="nav-item">
<a class="nav-link" href="billing.php">Billing</a>
</li>
<li class="nav-item">
<a class="nav-link" href="logout.php">Logout</a>
</li>
<?php else: ?>
<li class="nav-item">
<a class="nav-link" href="login.php">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="register.php">Register</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">

39
pricing.php Normal file
View File

@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/partials/header.php';
require_once __DIR__ . '/db/config.php';
$pdo = db();
$stmt = $pdo->query("SELECT * FROM plans ORDER BY price");
$plans = $stmt->fetchAll();
?>
<div class="container">
<div class="text-center my-5">
<h1>Purchase Credits</h1>
<p class="lead">One credit buys you one image analysis.</p>
</div>
<div class="row">
<?php foreach ($plans as $plan): ?>
<div class="col-md-4">
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h4 class="my-0 fw-normal"><?= htmlspecialchars($plan['name']) ?></h4>
</div>
<div class="card-body">
<h1 class="card-title pricing-card-title">$<?= htmlspecialchars(number_format($plan['price'], 2)) ?></h1>
<ul class="list-unstyled mt-3 mb-4">
<li><strong><?= htmlspecialchars($plan['credits_awarded']) ?></strong> credits</li>
<li>Never expire</li>
<li>Use them anytime</li>
</ul>
<a href="checkout.php?price_id=<?= htmlspecialchars($plan['stripe_price_id']) ?>" class="w-100 btn btn-lg btn-primary">Buy Now</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php require_once __DIR__ . '/partials/footer.php'; ?>

77
register.php Normal file
View File

@ -0,0 +1,77 @@
<?php
require_once 'db/config.php';
require_once 'partials/header.php';
$errors = [];
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'A valid email is required.';
}
if (empty($password) || strlen($password) < 8) {
$errors[] = 'Password must be at least 8 characters long.';
}
if (empty($errors)) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
if ($stmt->fetch()) {
$errors[] = 'Email already exists.';
} else {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (email, password) VALUES (?, ?)");
$stmt->execute([$email, $hashed_password]);
$success = 'Registration successful! You can now <a href="login.php">log in</a>.';
}
} catch (PDOException $e) {
$errors[] = 'Database error: ' . $e->getMessage();
}
}
}
?>
<div class="auth-form">
<h1>Register</h1>
<?php if (!empty($errors)):
?>
<div class="alert alert-danger">
<?php foreach ($errors as $error):
?>
<p><?php echo htmlspecialchars($error); ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($success):
?>
<div class="alert alert-success">
<p><?php echo $success; ?></p>
</div>
<?php else:
?>
<form action="register.php" method="post">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Register</button>
</form>
<p>Already have an account? <a href="login.php">Login here</a>.</p>
<?php endif; ?>
</div>
<?php
require_once 'partials/footer.php';
?>

94
report.php Normal file
View File

@ -0,0 +1,94 @@
<?php
session_start();
require_once 'db/config.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit();
}
// Check for active subscription
if (!hasActiveSubscription($_SESSION['user_id'])) {
// Redirect to billing page if no active subscription
header('Location: billing.php');
exit();
}
if (!isset($_GET['id'])) {
header('Location: index.php');
exit();
}
$uploadId = $_GET['id'];
$userId = $_SESSION['user_id'];
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM uploads WHERE id = ? AND user_id = ? AND status = 'completed'");
$stmt->execute([$uploadId, $userId]);
$upload = $stmt->fetch();
if (!$upload) {
// Redirect if the upload doesn't exist, doesn't belong to the user, or isn't completed
header('Location: index.php');
exit();
}
$analysisResult = json_decode($upload['analysis_result'], true);
$imagePath = htmlspecialchars($upload['file_path']);
list($width, $height) = getimagesize($upload['file_path']);
include 'partials/header.php';
?>
<main class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3">Analysis Report</h2>
<a href="index.php" class="btn btn-outline-secondary">Back to Dashboard</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-7">
<h4 class="mb-3">Vehicle Image</h4>
<div id="image-container" class="position-relative" style="width: <?php echo $width; ?>px; max-width: 100%;">
<img src="<?php echo $imagePath; ?>" class="img-fluid" alt="Analyzed Vehicle Image">
<?php if (isset($analysisResult['bounding_box'])):
$box = $analysisResult['bounding_box'];
// Calculate percentages for responsive scaling
$left = ($box['x'] / $width) * 100;
$top = ($box['y'] / $height) * 100;
$boxWidth = ($box['width'] / $width) * 100;
$boxHeight = ($box['height'] / $height) * 100;
?>
<div class="bounding-box" style="left: <?php echo $left; ?>%; top: <?php echo $top; ?>%; width: <?php echo $boxWidth; ?>%; height: <?php echo $boxHeight; ?>%;"></div>
<?php endif; ?>
</div>
</div>
<div class="col-md-5">
<h4 class="mb-3">Report Details</h4>
<?php if ($analysisResult): ?>
<div class="table-responsive">
<table class="table table-bordered">
<?php foreach ($analysisResult as $key => $value):
if (is_array($value)) continue; // Skip arrays like bounding_box
?>
<tr>
<th class="w-50"><strong><?php echo ucfirst(str_replace('_', ' ', htmlspecialchars($key))); ?></strong></th>
<td><?php echo is_bool($value) ? ($value ? 'Yes' : 'No') : htmlspecialchars($value); ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<h5 class="mt-4">Raw JSON Output</h5>
<pre class="bg-light p-3 rounded"><code><?php echo json_encode($analysisResult, JSON_PRETTY_PRINT); ?></code></pre>
<?php else: ?>
<p>No analysis data available.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
</main>
<?php include 'partials/footer.php'; ?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

92
webhook.php Normal file
View File

@ -0,0 +1,92 @@
<?php
require_once 'vendor/autoload.php';
require_once 'db/config.php';
// Get Stripe keys from environment
$stripeSecretKey = getenv('STRIPE_SECRET_KEY');
$webhookSecret = getenv('STRIPE_WEBHOOK_SECRET');
if (!$stripeSecretKey || !$webhookSecret) {
http_response_code(500);
error_log('Stripe keys are not configured.');
exit('Configuration error.');
}
\Stripe\Stripe::setApiKey($stripeSecretKey);
$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = null;
try {
$event = \Stripe\Webhook::constructEvent(
$payload, $sig_header, $webhookSecret
);
} catch(\UnexpectedValueException $e) {
http_response_code(400);
exit(); // Invalid payload
} catch(\Stripe\Exception\SignatureVerificationException $e) {
http_response_code(400);
exit(); // Invalid signature
}
// Handle the event
switch ($event->type) {
case 'checkout.session.completed':
$session = $event->data->object;
handleCheckoutSession($session);
break;
default:
// Unexpected event type
error_log('Received unknown event type ' . $event->type);
}
http_response_code(200);
function handleCheckoutSession($session) {
$userId = $session->client_reference_id;
$stripeChargeId = $session->payment_intent; // Using payment_intent as a proxy for charge ID
if (!$userId) {
error_log('Webhook Error: No client_reference_id in checkout.session.completed');
return;
}
try {
$pdo = db();
// Retrieve the line items to find out what was purchased
$line_items = \Stripe\Checkout\Session::allLineItems($session->id, ['limit' => 1]);
$priceId = $line_items->data[0]->price->id;
// Get plan details from our database
$stmt = $pdo->prepare("SELECT id, credits_awarded, price FROM plans WHERE stripe_price_id = ?");
$stmt->execute([$priceId]);
$plan = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$plan) {
error_log("Webhook Error: Plan with price ID {$priceId} not found in database.");
return;
}
$planId = $plan['id'];
$creditsPurchased = $plan['credits_awarded'];
$amountPaid = $plan['price'];
// Record the purchase
$sql = "INSERT INTO purchases (user_id, plan_id, stripe_charge_id, credits_purchased, amount_paid) VALUES (?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$userId, $planId, $stripeChargeId, $creditsPurchased, $amountPaid]);
// Add credits to the user's account
$sql = "UPDATE users SET credits = credits + ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$creditsPurchased, $userId]);
} catch (\Stripe\Exception\ApiErrorException $e) {
error_log("Stripe API Error in webhook: " . $e->getMessage());
} catch (PDOException $e) {
error_log("Database error in webhook: " . $e->getMessage());
}
}