mock backend
This commit is contained in:
parent
9470b78cdc
commit
31ce78aca9
@ -41,4 +41,79 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
].join('');
|
].join('');
|
||||||
alertContainer.append(wrapper);
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
70
db/migrate.php
Normal file
70
db/migrate.php
Normal 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");
|
||||||
|
}
|
||||||
8
db/migrations/001_create_products_table.sql
Normal file
8
db/migrations/001_create_products_table.sql
Normal 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`)
|
||||||
|
);
|
||||||
22
index.php
22
index.php
@ -5,12 +5,28 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>PricePal SA</h1>
|
<h1>PricePal SA</h1>
|
||||||
<p>Your smart shopping companion for comparing prices across South African retailers.</p>
|
<p>Your smart shopping companion for comparing prices across South African retailers.</p>
|
||||||
<a href="#" class="btn btn-light btn-lg">
|
<form id="searchForm" class="mt-4">
|
||||||
<i class="bi bi-google-play"></i> Download on Google Play
|
<div class="input-group input-group-lg">
|
||||||
</a>
|
<input type="search" id="searchInput" class="form-control" placeholder="Search for a product (e.g., Milk, Bread...)" aria-label="Search for a product">
|
||||||
|
<button class="btn btn-primary" type="submit" id="searchButton">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Search Results Section -->
|
||||||
|
<section id="searchResults" class="section" style="display: none;">
|
||||||
|
<div class="container">
|
||||||
|
<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 -->
|
<!-- Features Section -->
|
||||||
<section id="features" class="section">
|
<section id="features" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
45
search.php
Normal file
45
search.php
Normal 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()]);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user