Compare commits

...

2 Commits

Author SHA1 Message Date
Flatlogic Bot
31ce78aca9 mock backend 2025-11-25 07:51:50 +00:00
Flatlogic Bot
9470b78cdc 1st attempt 2025-11-25 07:44:56 +00:00
12 changed files with 565 additions and 148 deletions

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

@ -0,0 +1,73 @@
body {
font-family: 'system-ui', -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #F8F9FA;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,.05);
}
.hero {
background: linear-gradient(135deg, #0D6EFD, #0dcaf0);
color: white;
padding: 6rem 0;
text-align: center;
}
.hero h1 {
font-weight: 700;
font-size: 3.5rem;
}
.hero p {
font-size: 1.25rem;
margin-bottom: 2rem;
}
.btn-primary {
background-color: #0D6EFD;
border-color: #0D6EFD;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
font-weight: 500;
}
.section {
padding: 4rem 0;
}
.feature-icon {
font-size: 3rem;
color: #0D6EFD;
}
.card {
border: none;
border-radius: 0.5rem;
box-shadow: 0 4px 8px rgba(0,0,0,.05);
transition: transform .2s;
}
.card:hover {
transform: translateY(-5px);
}
#contact {
background-color: #FFFFFF;
}
.footer {
background-color: #343A40;
color: white;
padding: 2rem 0;
}
.footer a {
color: #adb5bd;
text-decoration: none;
}
.footer a:hover {
color: white;
}

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

@ -0,0 +1,119 @@
document.addEventListener('DOMContentLoaded', function () {
const contactForm = document.getElementById('contactForm');
if (contactForm) {
contactForm.addEventListener('submit', function (e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const alertContainer = document.getElementById('form-alerts');
alertContainer.innerHTML = '';
fetch('contact.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Message sent successfully! We will get back to you shortly.');
form.reset();
} else {
showAlert('danger', 'An error occurred: ' + (data.error || 'Please try again.'));
}
})
.catch(error => {
showAlert('danger', 'A network error occurred. Please try again.');
console.error('Error:', error);
});
});
}
function showAlert(type, message) {
const alertContainer = document.getElementById('form-alerts');
const wrapper = document.createElement('div');
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
` <div>${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('');
alertContainer.append(wrapper);
}
// --- Search Functionality ---
const searchForm = document.getElementById('searchForm');
if (searchForm) {
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const searchResultsSection = document.getElementById('searchResults');
const resultsContainer = document.getElementById('resultsContainer');
const noResults = document.getElementById('noResults');
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
const query = searchInput.value.trim();
if (query === '') {
return;
}
// Show loading state
searchButton.disabled = true;
searchButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Searching...';
resultsContainer.innerHTML = '';
noResults.style.display = 'none';
searchResultsSection.style.display = 'block';
fetch(`search.php?q=${encodeURIComponent(query)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.length > 0) {
displayResults(data);
} else {
noResults.style.display = 'block';
}
})
.catch(error => {
console.error('Search error:', error);
resultsContainer.innerHTML = '<p class="text-danger text-center">An error occurred while searching. Please try again.</p>';
})
.finally(() => {
// Restore button state
searchButton.disabled = false;
searchButton.innerHTML = 'Search';
});
});
function displayResults(products) {
products.forEach(product => {
const pricesHtml = product.prices.map(p => `
<li class="list-group-item d-flex justify-content-between align-items-center">
${p.retailer}
<span class="badge bg-primary rounded-pill">R ${parseFloat(p.price).toFixed(2)}</span>
</li>
`).join('');
const productCard = `
<div class="col-md-6 col-lg-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">${product.name}</h5>
<ul class="list-group list-group-flush">
${pricesHtml}
</ul>
</div>
</div>
</div>
`;
resultsContainer.innerHTML += productCard;
});
}
}
});

36
contact.php Normal file
View File

@ -0,0 +1,36 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/mail/MailService.php';
$response = ['success' => false];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = isset($_POST['name']) ? trim($_POST['name']) : '';
$email = isset($_POST['email']) ? trim($_POST['email']) : '';
$message = isset($_POST['message']) ? trim($_POST['message']) : '';
if (!empty($name) && filter_var($email, FILTER_VALIDATE_EMAIL) && !empty($message)) {
// The 'to' address can be omitted to use the default from .env
// Or specified directly. For this example, we'll use a placeholder
// that should be configured in a real environment.
$to = getenv('MAIL_TO') ?: 'support@yourdomain.com';
$subject = 'New Contact Form Submission';
$mailResult = MailService::sendContactMessage($name, $email, $message, $to, $subject);
if (!empty($mailResult['success'])) {
$response['success'] = true;
} else {
$response['error'] = $mailResult['error'] ?? 'Failed to send email.';
// In a real app, you would log this error.
// error_log('MailService Error: ' . $response['error']);
}
} else {
$response['error'] = 'Invalid input. Please fill out all fields correctly.';
}
} else {
$response['error'] = 'Invalid request method.';
}
echo json_encode($response);

70
db/migrate.php Normal file
View File

@ -0,0 +1,70 @@
<?php
// Simple, idempotent migration and seeding script.
require_once __DIR__ . '/config.php';
function run_migrations(PDO $pdo): void
{
$migrationsDir = __DIR__ . '/migrations';
if (!is_dir($migrationsDir)) {
echo "Migrations directory not found.\n";
return;
}
$files = glob($migrationsDir . '/*.sql');
sort($files);
foreach ($files as $file) {
echo "Running migration: " . basename($file) . "...\n";
$sql = file_get_contents($file);
if ($sql === false) {
echo "Failed to read migration file: " . basename($file) . "\n";
continue;
}
try {
$pdo->exec($sql);
echo "Success.\n";
} catch (PDOException $e) {
echo "Error running migration " . basename($file) . ": " . $e->getMessage() . "\n";
}
}
}
function seed_data(PDO $pdo): void
{
$products = [
['name' => 'Milk 1L', 'retailer' => 'Pick n Pay', 'price' => 25.99],
['name' => 'Milk 1L', 'retailer' => 'Checkers', 'price' => 24.99],
['name' => 'Bread (White)', 'retailer' => 'Pick n Pay', 'price' => 15.50],
['name' => 'Bread (White)', 'retailer' => 'Checkers', 'price' => 14.99],
['name' => 'Eggs (12 pack)', 'retailer' => 'Pick n Pay', 'price' => 32.00],
['name' => 'Eggs (12 pack)', 'retailer' => 'Checkers', 'price' => 31.50],
['name' => 'Cheese (250g)', 'retailer' => 'Pick n Pay', 'price' => 55.00],
['name' => 'Cheese (250g)', 'retailer' => 'Checkers', 'price' => 52.99],
];
$stmt = $pdo->prepare(
'INSERT INTO products (name, retailer, price) VALUES (:name, :retailer, :price)
ON DUPLICATE KEY UPDATE price = VALUES(price)'
);
echo "\nSeeding data...\n";
foreach ($products as $product) {
try {
$stmt->execute($product);
echo "Seeded/Updated: " . $product['name'] . " (" . $product['retailer'] . ")\n";
} catch (PDOException $e) {
echo "Error seeding " . $product['name'] . ": " . $e->getMessage() . "\n";
}
}
echo "Seeding complete.\n";
}
try {
$pdo = db();
run_migrations($pdo);
seed_data($pdo);
echo "\nDatabase setup complete!\n";
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage() . "\n");
}

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
retailer VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `product_retailer` (`name`,`retailer`)
);

16
footer.php Normal file
View File

@ -0,0 +1,16 @@
<footer class="footer">
<div class="container text-center">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'PricePal'); ?>. All Rights Reserved.</p>
<ul class="list-inline">
<li class="list-inline-item"><a href="privacy.php">Privacy Policy</a></li>
<li class="list-inline-item mx-2">|</li>
<li class="list-inline-item"><a href="terms.php">Terms of Service</a></li>
</ul>
</div>
</footer>
<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>

30
header.php Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Price Comparison App'); ?></title>
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'Compare prices across major retailers.'); ?>">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-white sticky-top">
<div class="container">
<a class="navbar-brand" href="index.php"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'PricePal'); ?></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#features">Features</a></li>
<li class="nav-item"><a class="nav-link" href="index.php#contact">Contact</a></li>
<li class="nav-item"><a class="nav-link" href="legal.php">Legal</a></li>
</ul>
</div>
</div>
</nav>

250
index.php
View File

@ -1,150 +1,106 @@
<?php <?php require_once 'header.php'; ?>
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; <!-- Hero Section -->
$now = date('Y-m-d H:i:s'); <header class="hero">
?> <div class="container">
<!doctype html> <h1>PricePal SA</h1>
<html lang="en"> <p>Your smart shopping companion for comparing prices across South African retailers.</p>
<head> <form id="searchForm" class="mt-4">
<meta charset="utf-8" /> <div class="input-group input-group-lg">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <input type="search" id="searchInput" class="form-control" placeholder="Search for a product (e.g., Milk, Bread...)" aria-label="Search for a product">
<title>New Style</title> <button class="btn btn-primary" type="submit" id="searchButton">Search</button>
<?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>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> </form>
<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> </header>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <!-- Search Results Section -->
</footer> <section id="searchResults" class="section" style="display: none;">
</body> <div class="container">
</html> <div class="text-center mb-5">
<h2>Search Results</h2>
</div>
<div id="resultsContainer" class="row g-4"></div>
<div id="noResults" class="text-center" style="display: none;">
<p class="lead">No products found matching your search.</p>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="section">
<div class="container">
<div class="text-center mb-5">
<h2>Why You'll Love PricePal</h2>
<p class="lead">All the features you need for smarter shopping.</p>
</div>
<div class="row g-4">
<!-- Feature 1 -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 text-center p-4">
<div class="feature-icon mb-3"><i class="bi bi-search"></i></div>
<h5 class="card-title">Instant Search</h5>
<p class="card-text">Get mock price comparisons from top retailers with a single tap.</p>
</div>
</div>
<!-- Feature 2 -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 text-center p-4">
<div class="feature-icon mb-3"><i class="bi bi-ui-checks-grid"></i></div>
<h5 class="card-title">Retailer Selection</h5>
<p class="card-text">Start with 2 default retailers and expand to see more options.</p>
</div>
</div>
<!-- Feature 3 -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 text-center p-4">
<div class="feature-icon mb-3"><i class="bi bi-heart"></i></div>
<h5 class="card-title">Save Favorites</h5>
<p class="card-text">Keep a list of your favorite products for quick price checks anytime.</p>
</div>
</div>
<!-- Feature 4 -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 text-center p-4">
<div class="feature-icon mb-3"><i class="bi bi-shield-check"></i></div>
<h5 class="card-title">Ethical & Transparent</h5>
<p class="card-text">Clear disclaimers about our mock data usage on every search.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section id="contact" class="section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="text-center mb-5">
<h2>Get In Touch</h2>
<p class="lead">Have questions or feedback? We'd love to hear from you.</p>
</div>
<div id="form-alerts"></div>
<form id="contactForm">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" id="message" name="message" rows="5" required></textarea>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">Send Message</button>
</div>
</form>
</div>
</div>
</div>
</section>
<?php require_once 'footer.php'; ?>

19
legal.php Normal file
View File

@ -0,0 +1,19 @@
<?php require_once 'header.php'; ?>
<main class="container my-5">
<h1>Legal & Disclaimer</h1>
<hr>
<p>This application is for demonstration purposes only. The data presented is mock data and does not represent real-time prices from any retailer.</p>
<h2>Data Sourcing</h2>
<p>All product and pricing information displayed within this application is generated randomly and is not sourced from Pick n Pay, Woolworths, Checkers, Game, Makro, or any other retailer. Any resemblance to actual products or prices is purely coincidental.</p>
<h2>Ethical Considerations</h2>
<p>We are committed to ethical practices. This application is designed as a proof-of-concept and should not be used for making actual purchasing decisions. We respect the intellectual property and business operations of all retailers.</p>
<h2>Limitation of Liability</h2>
<p>The developers and owners of this application shall not be held liable for any decisions made based on the information presented herein. Users are advised to consult official retailer websites for accurate and up-to-date pricing.</p>
</main>
<?php require_once 'footer.php'; ?>

26
privacy.php Normal file
View File

@ -0,0 +1,26 @@
<?php require_once 'header.php'; ?>
<main class="container my-5">
<h1>Privacy Policy</h1>
<hr>
<p>Your privacy is important to us. It is our policy to respect your privacy regarding any information we may collect from you across our application.</p>
<h2>Information We Collect</h2>
<p>We only ask for personal information when we truly need it to provide a service to you. For the contact form, we collect your name and email address to respond to your inquiry. For saved favorite products, the data is stored locally on your device and is not transmitted to our servers.</p>
<h2>Use of Information</h2>
<p>We use the collected information to:</p>
<ul>
<li>Respond to your inquiries submitted through the contact form.</li>
<li>Provide and maintain the application's features, such as saving favorite products locally.</li>
</ul>
<h2>Data Storage</h2>
<p>We only retain collected information for as long as necessary to provide you with your requested service. What data we store, well protect within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use or modification.</p>
<h2>Third Parties</h2>
<p>We do not share any personally identifying information publicly or with third-parties, except when required to by law.</p>
</main>
<?php require_once 'footer.php'; ?>

45
search.php Normal file
View File

@ -0,0 +1,45 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/db/config.php';
$query = $_GET['q'] ?? '';
if (empty($query)) {
echo json_encode([]);
exit;
}
try {
$pdo = db();
// Find all products matching the query
$stmt = $pdo->prepare(
'SELECT name, retailer, price FROM products WHERE name LIKE :query ORDER BY name, price'
);
$stmt->execute(['query' => '%' . $query . '%']);
$results = $stmt->fetchAll();
// Group prices by product name
$products = [];
foreach ($results as $row) {
if (!isset($products[$row['name']])) {
$products[$row['name']] = [
'name' => $row['name'],
'prices' => []
];
}
$products[$row['name']]['prices'][] = [
'retailer' => $row['retailer'],
'price' => $row['price']
];
}
// Return as a simple array of products
echo json_encode(array_values($products));
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}

19
terms.php Normal file
View File

@ -0,0 +1,19 @@
<?php require_once 'header.php'; ?>
<main class="container my-5">
<h1>Terms of Service</h1>
<hr>
<p>By using this application, you are agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable local laws.</p>
<h2>1. Use License</h2>
<p>Permission is granted to temporarily download one copy of the materials (information or software) on this application for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title.</p>
<h2>2. Disclaimer</h2>
<p>The materials on this application are provided on an 'as is' basis. We make no warranties, expressed or implied, and hereby disclaim and negate all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.</p>
<h2>3. Limitations</h2>
<p>In no event shall the application owners or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on this application.</p>
</main>
<?php require_once 'footer.php'; ?>