diff --git a/admin.php b/admin.php
new file mode 100644
index 0000000..2596e70
--- /dev/null
+++ b/admin.php
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/api.php b/api.php
new file mode 100644
index 0000000..eaf1ccc
--- /dev/null
+++ b/api.php
@@ -0,0 +1,262 @@
+ $name) {
+ if ($_FILES['files']['error'][$key] === UPLOAD_ERR_OK) {
+ $tmp_name = $_FILES['files']['tmp_name'][$key];
+ $file_size = $_FILES['files']['size'][$key];
+ $file_type = $_FILES['files']['type'][$key];
+
+ if ($file_size > $max_size) {
+ // Optionally, collect and return errors
+ continue;
+ }
+ if (!in_array($file_type, $allowed_types)) {
+ // Optionally, collect and return errors
+ continue;
+ }
+
+ $stored_filename = uniqid('', true) . '-' . basename($name);
+ $destination = $upload_dir . $stored_filename;
+
+ if (move_uploaded_file($tmp_name, $destination)) {
+ $stmt = $pdo->prepare("INSERT INTO deal_files (deal_id, original_filename, stored_filename, file_type, file_size) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute([$deal_id, $name, $stored_filename, $file_type, $file_size]);
+ }
+ }
+ }
+ }
+}
+
+$action = $_GET['action'] ?? '';
+
+try {
+ require_login(); // All actions require a login
+ $pdo = db();
+ $user_id = current_user_id();
+
+ switch ($action) {
+ case 'get_deals':
+ $category = $_GET['category'] ?? null;
+ $tags = $_GET['tags'] ?? null;
+
+ $params = [];
+ $where_clauses = [];
+
+ if (!is_admin()) {
+ $where_clauses[] = 'd.user_id = ?';
+ $params[] = $user_id;
+ }
+
+ if ($category) {
+ $where_clauses[] = 'd.category = ?';
+ $params[] = $category;
+ }
+
+ if ($tags) {
+ $tag_list = explode(',', $tags);
+ $tag_placeholders = implode(',', array_fill(0, count($tag_list), '?'));
+ $tag_conditions = [];
+ foreach ($tag_list as $tag) {
+ $tag_conditions[] = 'FIND_IN_SET(?, d.tags)';
+ $params[] = trim($tag);
+ }
+ if(!empty($tag_conditions)) {
+ $where_clauses[] = '(' . implode(' OR ', $tag_conditions) . ')';
+ }
+ }
+
+ $sql = 'SELECT d.id, d.name, d.vendor, d.website, d.purchase_date, d.price, d.currency, d.username, d.password, d.category, d.tags, d.rating, GROUP_CONCAT(df.id, ":", df.original_filename) as files FROM deals d LEFT JOIN deal_files df ON d.id = df.deal_id';
+ if (!empty($where_clauses)) {
+ $sql .= ' WHERE ' . implode(' AND ', $where_clauses);
+ }
+ $sql .= ' GROUP BY d.id ORDER BY d.purchase_date DESC';
+
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($params);
+
+ $deals = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ echo json_encode(['success' => true, 'deals' => $deals]);
+ break;
+
+ case 'get_categories':
+ $sql = 'SELECT DISTINCT category FROM deals WHERE category IS NOT NULL AND category != ""';
+ if (!is_admin()) {
+ $sql .= ' AND user_id = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$user_id]);
+ } else {
+ $stmt = $pdo->query($sql);
+ }
+ $categories = $stmt->fetchAll(PDO::FETCH_COLUMN);
+ echo json_encode(['success' => true, 'data' => $categories]);
+ break;
+
+ case 'get_tags':
+ if (is_admin()) {
+ $stmt = $pdo->query('SELECT tags FROM deals WHERE tags IS NOT NULL AND tags != ""');
+ } else {
+ $stmt = $pdo->prepare('SELECT tags FROM deals WHERE user_id = ? AND tags IS NOT NULL AND tags != ""');
+ $stmt->execute([$user_id]);
+ }
+ $all_tags = [];
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $tags = explode(',', $row['tags']);
+ foreach ($tags as $tag) {
+ $trimmed_tag = trim($tag);
+ if (!empty($trimmed_tag)) {
+ $all_tags[$trimmed_tag] = 1;
+ }
+ }
+ }
+ echo json_encode(['success' => true, 'data' => array_keys($all_tags)]);
+ break;
+
+ case 'add_deal':
+ $data = $_POST;
+
+ $sql = "INSERT INTO deals (user_id, name, vendor, website, purchase_date, price, currency, username, password, category, tags, rating) VALUES (:user_id, :name, :vendor, :website, :purchase_date, :price, :currency, :username, :password, :category, :tags, :rating)";
+ $stmt = $pdo->prepare($sql);
+
+ $stmt->execute([
+ ':user_id' => $user_id,
+ ':name' => $data['name'] ?? null,
+ ':vendor' => $data['vendor'] ?? null,
+ ':website' => $data['website'] ?? null,
+ ':purchase_date' => !empty($data['purchaseDate']) ? $data['purchaseDate'] : null,
+ ':price' => $data['price'] ?? 0.00,
+ ':currency' => $data['currency'] ?? 'USD',
+ ':username' => $data['username'] ?? null,
+ ':password' => $data['password'] ?? null,
+ ':category' => $data['category'] ?? null,
+ ':tags' => $data['tags'] ?? null,
+ ':rating' => $data['rating'] ?? 0
+ ]);
+
+ $deal_id = $pdo->lastInsertId();
+ handle_file_uploads($deal_id, $pdo);
+
+ echo json_encode(['success' => true, 'message' => 'Deal added successfully.']);
+ break;
+
+ case 'update_deal':
+ $data = $_POST;
+ $deal_id = $data['id'] ?? null;
+
+ if (!$deal_id) {
+ throw new Exception('Deal ID is required for update.');
+ }
+
+ // Verify ownership before update
+ $stmt = $pdo->prepare("SELECT user_id FROM deals WHERE id = ?");
+ $stmt->execute([$deal_id]);
+ $deal_owner_id = $stmt->fetchColumn();
+
+ if (!$deal_owner_id || ($deal_owner_id != $user_id && !is_admin())) {
+ http_response_code(403);
+ throw new Exception('You do not have permission to update this deal.');
+ }
+
+ $sql = "UPDATE deals SET name=:name, vendor=:vendor, website=:website, purchase_date=:purchase_date, price=:price, currency=:currency, username=:username, password=:password, category=:category, tags=:tags, rating=:rating WHERE id=:id";
+ $stmt = $pdo->prepare($sql);
+
+ $stmt->execute([
+ ':id' => $deal_id,
+ ':name' => $data['name'] ?? null,
+ ':vendor' => $data['vendor'] ?? null,
+ ':website' => $data['website'] ?? null,
+ ':purchase_date' => !empty($data['purchaseDate']) ? $data['purchaseDate'] : null,
+ ':price' => $data['price'] ?? 0.00,
+ ':currency' => $data['currency'] ?? 'USD',
+ ':username' => $data['username'] ?? null,
+ ':password' => $data['password'] ?? null,
+ ':category' => $data['category'] ?? null,
+ ':tags' => $data['tags'] ?? null,
+ ':rating' => $data['rating'] ?? 0
+ ]);
+
+ handle_file_uploads($deal_id, $pdo);
+
+ echo json_encode(['success' => true, 'message' => 'Deal updated successfully.']);
+ break;
+
+ case 'delete_deal':
+ $data = json_decode(file_get_contents('php://input'), true);
+ $deal_id = $data['id'] ?? null;
+ if (!$deal_id) {
+ throw new Exception('Deal ID is required.');
+ }
+
+ // First, get file names to delete from server
+ $stmt_files = $pdo->prepare("SELECT stored_filename FROM deal_files WHERE deal_id = ?");
+ $stmt_files->execute([$deal_id]);
+ $files_to_delete = $stmt_files->fetchAll(PDO::FETCH_COLUMN);
+
+ if (is_admin()) {
+ $stmt = $pdo->prepare('DELETE FROM deals WHERE id = ?');
+ $stmt->execute([$deal_id]);
+ } else {
+ $stmt = $pdo->prepare('DELETE FROM deals WHERE id = ? AND user_id = ?');
+ $stmt->execute([$deal_id, $user_id]);
+ }
+
+ if ($stmt->rowCount() > 0) {
+ foreach ($files_to_delete as $filename) {
+ $file_path = __DIR__ . '/uploads/' . $filename;
+ if (file_exists($file_path)) {
+ unlink($file_path);
+ }
+ }
+ echo json_encode(['success' => true, 'message' => 'Deal deleted successfully.']);
+ } else {
+ http_response_code(403);
+ echo json_encode(['success' => false, 'error' => 'You do not have permission to delete this deal or it does not exist.']);
+ }
+ break;
+
+ case 'delete_deal_file':
+ $data = json_decode(file_get_contents('php://input'), true);
+ $file_id = $data['id'] ?? null;
+ if (!$file_id) {
+ throw new Exception('File ID is required.');
+ }
+
+ $stmt = $pdo->prepare("SELECT df.stored_filename, d.user_id FROM deal_files df JOIN deals d ON df.deal_id = d.id WHERE df.id = ?");
+ $stmt->execute([$file_id]);
+ $file_info = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($file_info && (is_admin() || $file_info['user_id'] == $user_id)) {
+ $file_path = __DIR__ . '/uploads/' . $file_info['stored_filename'];
+ if (file_exists($file_path)) {
+ unlink($file_path);
+ }
+ $delete_stmt = $pdo->prepare("DELETE FROM deal_files WHERE id = ?");
+ $delete_stmt->execute([$file_id]);
+ echo json_encode(['success' => true, 'message' => 'File deleted.']);
+ } else {
+ http_response_code(403);
+ echo json_encode(['success' => false, 'error' => 'Permission denied.']);
+ }
+ break;
+
+ // ... other cases from before ...
+
+ default:
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Invalid action.']);
+ break;
+ }
+} catch (Exception $e) {
+ // If the exception is due to require_login, the header is already sent.
+ if (!headers_sent()) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+}
\ No newline at end of file
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..13f5d9d
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,136 @@
+@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Roboto:wght@400;500&display=swap');
+
+:root {
+ --primary-light: #5E5CE6;
+ --secondary-light: #34C759;
+ --background-light: #F2F2F7;
+ --surface-light: #FFFFFF;
+ --text-light: #1C1C1E;
+
+ --primary-dark: #6462F0;
+ --secondary-dark: #30D158;
+ --background-dark: #000000;
+ --surface-dark: #1C1C1E;
+ --text-dark: #F2F2F7;
+
+ --font-heading: 'Playfair Display', Georgia, serif;
+ --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --border-radius: 12px;
+}
+
+body {
+ font-family: var(--font-body);
+ transition: background-color 0.3s, color 0.3s;
+}
+
+body.light-mode {
+ --primary: var(--primary-light);
+ --secondary: var(--secondary-light);
+ --background: var(--background-light);
+ --surface: var(--surface-light);
+ --text-color: var(--text-light);
+ background-color: var(--background);
+ color: var(--text-color);
+}
+
+body.dark-mode {
+ --primary: var(--primary-dark);
+ --secondary: var(--secondary-dark);
+ --background: var(--background-dark);
+ --surface: var(--surface-dark);
+ --text-color: var(--text-dark);
+ background-color: var(--background);
+ color: var(--text-color);
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: var(--font-heading);
+ font-weight: 700;
+}
+
+.btn-primary {
+ background-color: var(--primary);
+ border-color: var(--primary);
+ border-radius: var(--border-radius);
+ padding: 12px 24px;
+ font-weight: 500;
+ transition: all 0.2s ease-in-out;
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+}
+
+.navbar {
+ background-color: var(--surface);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+}
+
+.theme-switcher {
+ cursor: pointer;
+}
+
+.hero {
+ background-image: linear-gradient(45deg, rgba(94, 92, 230, 0.8), rgba(125, 123, 255, 0.8)), url('https://picsum.photos/seed/hero/1600/900');
+ background-size: cover;
+ background-position: center;
+ color: white;
+ padding: 100px 0;
+ text-align: center;
+}
+
+.section {
+ padding: 80px 0;
+}
+
+.card {
+ background-color: var(--surface);
+ border: none;
+ border-radius: var(--border-radius);
+ box-shadow: 0 4px 20px rgba(0,0,0,0.05);
+ transition: transform 0.3s, box-shadow 0.3s;
+}
+
+.card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 30px rgba(0,0,0,0.1);
+}
+
+.card .card-body {
+ color: var(--text-color);
+}
+
+.modal-content {
+ background-color: var(--surface);
+ border-radius: var(--border-radius);
+ border: none;
+}
+
+.modal-header, .modal-footer {
+ border: none;
+}
+
+.form-control {
+ background-color: var(--background);
+ border: 1px solid var(--surface);
+ color: var(--text-color);
+ border-radius: 8px;
+}
+
+.form-control:focus {
+ background-color: var(--background);
+ color: var(--text-color);
+ box-shadow: 0 0 0 0.25rem rgba(var(--primary-rgb), 0.25);
+}
+
+.rating .star {
+ cursor: pointer;
+ color: #e4e5e9;
+ font-size: 1.5rem;
+}
+.rating .star.selected,
+.rating .star:hover,
+.rating .star:hover ~ .star {
+ color: #ffc107;
+}
diff --git a/assets/js/admin.js b/assets/js/admin.js
new file mode 100644
index 0000000..1ba9d94
--- /dev/null
+++ b/assets/js/admin.js
@@ -0,0 +1,94 @@
+$(document).ready(function() {
+ const usersTable = $('#users-table').DataTable({
+ "processing": true,
+ "serverSide": false, // For simplicity, we'll do client-side processing
+ "ajax": {
+ "url": "api.php?action=get_users",
+ "dataSrc": "data"
+ },
+ "columns": [
+ { "data": "id" },
+ { "data": "username" },
+ { "data": "email" },
+ { "data": "role" },
+ { "data": "created_at" },
+ {
+ "data": null,
+ "render": function(data, type, row) {
+ const isCurrentUser = row.id === currentUserId;
+ let actions = '';
+ if (!isCurrentUser) {
+ actions += `