Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
18
.htaccess
18
.htaccess
@ -0,0 +1,18 @@
|
||||
DirectoryIndex index.php index.html
|
||||
Options -Indexes
|
||||
Options -MultiViews
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# 0) Serve existing files/directories as-is
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
|
||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
||||
RewriteRule ^(.+?)/?$ $1.php [L]
|
||||
|
||||
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.+)/$ $1 [R=301,L]
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
$admin_timeout = 3600; // 1 hour
|
||||
|
||||
// Check if user is logged in and is an admin
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
|
||||
header('Location: ../login.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Session timeout logic
|
||||
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $admin_timeout)) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: ../login.php?timeout');
|
||||
exit();
|
||||
}
|
||||
|
||||
// IP binding logic
|
||||
if (isset($_SESSION['admin_ip_address']) && $_SESSION['admin_ip_address'] !== $_SERVER['REMOTE_ADDR']) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: ../login.php?ip_changed');
|
||||
exit();
|
||||
}
|
||||
|
||||
$_SESSION['last_activity'] = time(); // Update last activity time
|
||||
@ -1,151 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
$pdo = db();
|
||||
|
||||
// Handle form submissions for categories
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['add_category'])) {
|
||||
$name = trim($_POST['category_name']);
|
||||
if (!empty($name)) {
|
||||
$stmt = $pdo->prepare("INSERT INTO categories (name) VALUES (?)");
|
||||
$stmt->execute([$name]);
|
||||
}
|
||||
} elseif (isset($_POST['update_category'])) {
|
||||
$id = $_POST['category_id'];
|
||||
$name = trim($_POST['category_name']);
|
||||
$visibility = isset($_POST['visibility']) ? 1 : 0;
|
||||
$order = (int)$_POST['display_order'];
|
||||
$stmt = $pdo->prepare("UPDATE categories SET name = ?, visibility = ?, display_order = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $visibility, $order, $id]);
|
||||
} elseif (isset($_POST['delete_category'])) {
|
||||
$id = $_POST['category_id'];
|
||||
$stmt = $pdo->prepare("DELETE FROM categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
} elseif (isset($_POST['add_subcategory'])) {
|
||||
$name = trim($_POST['subcategory_name']);
|
||||
$category_id = $_POST['category_id'];
|
||||
if (!empty($name)) {
|
||||
$stmt = $pdo->prepare("INSERT INTO subcategories (category_id, name) VALUES (?, ?)");
|
||||
$stmt->execute([$category_id, $name]);
|
||||
}
|
||||
}
|
||||
header("Location: categories.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fetch all categories and subcategories
|
||||
$categories = $pdo->query("SELECT * FROM categories ORDER BY display_order ASC, name ASC")->fetchAll();
|
||||
$subcategories = [];
|
||||
$stmt = $pdo->query("SELECT * FROM subcategories ORDER BY name ASC");
|
||||
while ($row = $stmt->fetch()) {
|
||||
$subcategories[$row['category_id']][] = $row;
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage Categories - Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="../index.php">View Site</a>
|
||||
<a href="../logout.php">Logout</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<aside class="admin-nav">
|
||||
<h3>Menu</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link" href="index.php">Dashboard</a>
|
||||
<a class="nav-link active" href="categories.php">Categories</a>
|
||||
<a class="nav-link" href="users.php">Users</a>
|
||||
<a class="nav-link" href="links.php">Links</a>
|
||||
<a class="nav-link" href="settings.php">Settings</a>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<main class="content">
|
||||
<h2>Manage Categories</h2>
|
||||
|
||||
<!-- Add Category Form -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Add New Category</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="categories.php">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="category_name" placeholder="New category name" required>
|
||||
<button class="btn btn-primary" type="submit" name="add_category">Add Category</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category List -->
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="categories.php" class="d-flex align-items-center">
|
||||
<input type="hidden" name="category_id" value="<?php echo $category['id']; ?>">
|
||||
<div class="flex-grow-1">
|
||||
<input type="text" class="form-control" name="category_name" value="<?php echo htmlspecialchars($category['name']); ?>">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<label class="form-check-label me-2">Visible:</label>
|
||||
<input class="form-check-input" type="checkbox" name="visibility" <?php echo $category['visibility'] ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
<div class="ms-3" style="width: 80px;">
|
||||
<input type="number" class="form-control" name="display_order" value="<?php echo $category['display_order']; ?>">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<button type="submit" name="update_category" class="btn btn-sm btn-success">Save</button>
|
||||
<button type="submit" name="delete_category" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Del</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Subcategories -->
|
||||
<div class="mt-3 ms-4">
|
||||
<h6>Subcategories</h6>
|
||||
<ul>
|
||||
<?php if (isset($subcategories[$category['id']])): ?>
|
||||
<?php foreach ($subcategories[$category['id']] as $sub): ?>
|
||||
<li><?php echo htmlspecialchars($sub['name']); ?></li>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<li>No subcategories yet.</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<form method="POST" action="categories.php" class="input-group input-group-sm">
|
||||
<input type="hidden" name="category_id" value="<?php echo $category['id']; ?>">
|
||||
<input type="text" class="form-control" name="subcategory_name" placeholder="New subcategory" required>
|
||||
<button class="btn btn-outline-secondary" type="submit" name="add_subcategory">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,79 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// Fetch some basic stats for the dashboard
|
||||
$pdo = db();
|
||||
$user_count = $pdo->query("SELECT count(*) FROM users")->fetchColumn();
|
||||
$link_count = $pdo->query("SELECT count(*) FROM links")->fetchColumn();
|
||||
$category_count = $pdo->query("SELECT count(*) FROM categories")->fetchColumn();
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="../index.php">View Site</a>
|
||||
<a href="../logout.php">Logout</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<aside class="admin-nav">
|
||||
<h3>Menu</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link active" href="index.php">Dashboard</a>
|
||||
<a class="nav-link" href="categories.php">Categories</a>
|
||||
<a class="nav-link" href="users.php">Users</a>
|
||||
<a class="nav-link" href="links.php">Links</a>
|
||||
<a class="nav-link" href="settings.php">Settings</a>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<main class="content">
|
||||
<h2>Dashboard</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center p-3">
|
||||
<h3><?php echo $user_count; ?></h3>
|
||||
<p>Total Users</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center p-3">
|
||||
<h3><?php echo $link_count; ?></h3>
|
||||
<p>Total Links</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center p-3">
|
||||
<h3><?php echo $category_count; ?></h3>
|
||||
<p>Total Categories</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
418
admin/links.php
418
admin/links.php
@ -1,418 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
// Debug block for POST data - visible only to admin
|
||||
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
$pdo = db();
|
||||
|
||||
// CSRF Protection
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$response = ['success' => false, 'message' => 'Invalid request.', 'debug_post' => $_POST ?? [] ];
|
||||
|
||||
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
$response['message'] = 'CSRF token validation failed.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$link_id = $_POST['link_id'] ?? null;
|
||||
|
||||
if (!$link_id || !is_numeric($link_id)) {
|
||||
$response['message'] = 'Invalid Link ID.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'delete':
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM links WHERE id = ?");
|
||||
$stmt->execute([$link_id]);
|
||||
if ($stmt->rowCount()) {
|
||||
$response = ['success' => true, 'message' => 'Link deleted successfully.'];
|
||||
} else {
|
||||
$response['message'] = 'Link not found or could not be deleted.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$response['message'] = 'Database error: ' . $e->getMessage();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'toggle_status':
|
||||
$current_status = $_POST['current_status'] ?? '';
|
||||
$new_status = ($current_status === 'paused') ? 'approved' : 'paused'; // Toggle between paused and approved
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE links SET status = ? WHERE id = ?");
|
||||
$stmt->execute([$new_status, $link_id]);
|
||||
if ($stmt->rowCount()) {
|
||||
$response = ['success' => true, 'message' => 'Link status updated successfully to ' . $new_status . '.', 'new_status' => $new_status];
|
||||
} else {
|
||||
$response['message'] = 'Link not found or status could not be updated.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$response['message'] = 'Database error: ' . $e->getMessage();
|
||||
}
|
||||
break;
|
||||
|
||||
// Add 'edit' case later
|
||||
case 'edit':
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$subcategory_id = $_POST['subcategory_id'] ?? null;
|
||||
$status = $_POST['status'] ?? 'pending';
|
||||
|
||||
if (empty($title) || empty($url) || !filter_var($url, FILTER_VALIDATE_URL) || !is_numeric($subcategory_id)) {
|
||||
$response['message'] = 'Invalid input for editing link.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/ImageProcessor.php';
|
||||
|
||||
$current_link = $pdo->prepare("SELECT thumbnail_url FROM links WHERE id = ?");
|
||||
$current_link->execute([$link_id]);
|
||||
$current_thumbnail_url = $current_link->fetchColumn();
|
||||
|
||||
$new_thumbnail_url = $current_thumbnail_url;
|
||||
$remove_image = isset($_POST['remove_image']) && $_POST['remove_image'] === 'true';
|
||||
|
||||
// Handle image removal
|
||||
if ($remove_image) {
|
||||
if ($current_thumbnail_url && file_exists(__DIR__ . '/../' . $current_thumbnail_url)) {
|
||||
unlink(__DIR__ . '/../' . $current_thumbnail_url);
|
||||
}
|
||||
$new_thumbnail_url = null;
|
||||
}
|
||||
|
||||
// Handle new image upload
|
||||
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
|
||||
$upload_dir = __DIR__ . '/../assets/images/uploads/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$processor = new ImageProcessor($upload_dir);
|
||||
$uploaded_path = $processor->uploadAndResize($_FILES['thumbnail']);
|
||||
|
||||
if ($uploaded_path) {
|
||||
// Delete old thumbnail if a new one is uploaded
|
||||
if ($current_thumbnail_url && file_exists(__DIR__ . '/../' . $current_thumbnail_url)) {
|
||||
unlink(__DIR__ . '/../' . $current_thumbnail_url);
|
||||
}
|
||||
$new_thumbnail_url = 'assets/images/uploads/' . basename($uploaded_path);
|
||||
} else {
|
||||
$response['message'] = 'Failed to upload new thumbnail.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE links SET title = ?, url = ?, description = ?, subcategory_id = ?, status = ?, thumbnail_url = ? WHERE id = ?");
|
||||
$stmt->execute([$title, $url, $description, $subcategory_id, $status, $new_thumbnail_url, $link_id]);
|
||||
if ($stmt->rowCount()) {
|
||||
$response = ['success' => true, 'message' => 'Link updated successfully.'];
|
||||
} else {
|
||||
$response['message'] = 'Link not found or no changes made.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$response['message'] = 'Database error: ' . $e->getMessage();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['message'] = 'Unknown action.';
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$links = $pdo->query("SELECT l.*, u.username, s.name as subcategory_name, c.name as category_name
|
||||
FROM links l
|
||||
JOIN users u ON l.user_id = u.id
|
||||
JOIN subcategories s ON l.subcategory_id = s.id
|
||||
JOIN categories c ON s.category_id = c.id
|
||||
ORDER BY l.created_at DESC")->fetchAll();
|
||||
|
||||
// Fetch subcategories for the edit form
|
||||
$subcategories = $pdo->query("SELECT sc.id, sc.name AS subcategory_name, c.name AS category_name FROM subcategories sc JOIN categories c ON sc.category_id = c.id ORDER BY c.name, sc.name")->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage Links - Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="../index.php">View Site</a>
|
||||
<a href="../logout.php">Logout</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container-fluid my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<aside class="admin-nav">
|
||||
<h3>Menu</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link" href="index.php">Dashboard</a>
|
||||
<a class="nav-link" href="categories.php">Categories</a>
|
||||
<a class="nav-link" href="users.php">Users</a>
|
||||
<a class="nav-link active" href="links.php">Links</a>
|
||||
<a class="nav-link" href="settings.php">Settings</a>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<main class="content">
|
||||
<h2>Manage Links</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>URL</th>
|
||||
<th>Category</th>
|
||||
<th>User</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($links)): ?>
|
||||
<tr><td colspan="7">No links submitted yet.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($links as $link): ?>
|
||||
<tr data-id="<?php echo $link['id']; ?>" data-thumbnail-url="<?php echo htmlspecialchars($link['thumbnail_url']); ?>">
|
||||
<td class="link-title"><?php echo htmlspecialchars($link['title']); ?></td>
|
||||
<td class="link-url"><a href="<?php echo htmlspecialchars($link['url']); ?>" target="_blank"><?php echo htmlspecialchars(substr($link['url'], 0, 50)); ?>...</a></td>
|
||||
<td><?php echo htmlspecialchars($link['category_name']); ?> > <span class="link-subcategory-name" data-id="<?php echo $link['subcategory_id']; ?>"><?php echo htmlspecialchars($link['subcategory_name']); ?></span></td>
|
||||
<td><?php echo htmlspecialchars($link['username']); ?></td>
|
||||
<td><span class="badge bg-<?php echo $link['status'] === 'approved' ? 'success' : ($link['status'] === 'pending' ? 'warning' : ($link['status'] === 'paused' ? 'info' : 'danger')); ?> link-status"><?php echo htmlspecialchars($link['status']); ?></span></td>
|
||||
<td><?php echo date("Y-m-d", strtotime($link['created_at'])); ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary edit-link-btn" data-id="<?php echo $link['id']; ?>" data-bs-toggle="modal" data-bs-target="#editLinkModal">Edit</button>
|
||||
<button class="btn btn-sm btn-<?php echo $link['status'] === 'paused' ? 'success' : 'warning'; ?> toggle-status-btn" data-id="<?php echo $link['id']; ?>" data-status="<?php echo $link['status']; ?>"><?php echo $link['status'] === 'paused' ? 'Unpause' : 'Pause'; ?></button>
|
||||
<button class="btn btn-sm btn-danger delete-link-btn" data-id="<?php echo $link['id']; ?>">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Link Modal -->
|
||||
<div class="modal fade" id="editLinkModal" tabindex="-1" aria-labelledby="editLinkModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editLinkModalLabel">Edit Link</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editLinkForm">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="link_id" id="editLinkId">
|
||||
<div class="mb-3">
|
||||
<label for="editLinkTitle" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="editLinkTitle" name="title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editLinkUrl" class="form-label">URL</label>
|
||||
<input type="url" class="form-control" id="editLinkUrl" name="url" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editLinkDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="editLinkDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editLinkSubcategory" class="form-label">Subcategory</label>
|
||||
<select class="form-select" id="editLinkSubcategory" name="subcategory_id" required>
|
||||
<?php foreach ($subcategories as $sc): ?>
|
||||
<option value="<?php echo $sc['id']; ?>"><?php echo htmlspecialchars($sc['category_name'] . ' > ' . $sc['subcategory_name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editLinkStatus" class="form-label">Status</label>
|
||||
<select class="form-select" id="editLinkStatus" name="status" required>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="paused">Paused</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editLinkThumbnail" class="form-label">Current Thumbnail</label>
|
||||
<div id="currentThumbnailPreview" class="mb-2">
|
||||
<!-- Image will be loaded here by JS -->
|
||||
</div>
|
||||
<input type="file" class="form-control" id="editLinkThumbnail" name="thumbnail" accept="image/*">
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" value="true" id="removeCurrentThumbnail" name="remove_image">
|
||||
<label class="form-check-label" for="removeCurrentThumbnail">
|
||||
Remove current thumbnail
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
|
||||
document.querySelectorAll('.delete-link-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const linkId = this.dataset.id;
|
||||
if (confirm('Are you sure you want to delete this link?')) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'delete');
|
||||
formData.append('link_id', linkId);
|
||||
formData.append('csrf_token', csrfToken);
|
||||
sendAction(formData);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.toggle-status-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const linkId = this.dataset.id;
|
||||
const currentStatus = this.dataset.status;
|
||||
// Determine new status based on current for toggling between paused and approved
|
||||
// If current status is 'paused', next is 'approved'. Otherwise, 'paused'.
|
||||
const newStatus = (currentStatus === 'paused') ? 'approved' : 'paused';
|
||||
if (confirm(`Are you sure you want to ${newStatus === 'paused' ? 'pause' : 'unpause' } this link?`)) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'toggle_status');
|
||||
formData.append('link_id', linkId);
|
||||
formData.append('current_status', currentStatus);
|
||||
formData.append('csrf_token', csrfToken);
|
||||
sendAction(formData);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.edit-link-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const linkId = this.dataset.id;
|
||||
const row = this.closest('tr');
|
||||
|
||||
const linkId = this.dataset.id;
|
||||
const row = this.closest('tr');
|
||||
|
||||
document.getElementById('editLinkId').value = linkId;
|
||||
document.getElementById('editLinkTitle').value = row.querySelector('.link-title').textContent;
|
||||
document.getElementById('editLinkUrl').value = row.querySelector('.link-url a').href;
|
||||
document.getElementById('editLinkDescription').value = row.querySelector('.link-description') ? row.querySelector('.link-description').textContent : '';
|
||||
document.getElementById('editLinkStatus').value = row.querySelector('.link-status').textContent.trim();
|
||||
|
||||
// Select the correct subcategory in the dropdown
|
||||
const subcategoryId = row.querySelector('.link-subcategory-name').dataset.id;
|
||||
const subcategorySelect = document.getElementById('editLinkSubcategory');
|
||||
for (let i = 0; i < subcategorySelect.options.length; i++) {
|
||||
if (subcategorySelect.options[i].value == subcategoryId) {
|
||||
subcategorySelect.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset file input and checkbox
|
||||
document.getElementById('editLinkThumbnail').value = '';
|
||||
document.getElementById('removeCurrentThumbnail').checked = false;
|
||||
|
||||
// Display current thumbnail if it exists
|
||||
const currentThumbnailPreview = document.getElementById('currentThumbnailPreview');
|
||||
currentThumbnailPreview.innerHTML = ''; // Clear previous preview
|
||||
const thumbnailUrl = row.dataset.thumbnailUrl; // Assuming data-thumbnail-url attribute on tr
|
||||
if (thumbnailUrl && thumbnailUrl !== 'null') {
|
||||
const img = document.createElement('img');
|
||||
img.src = '../' + thumbnailUrl;
|
||||
img.alt = 'Current Thumbnail';
|
||||
img.style.maxWidth = '100px';
|
||||
img.style.maxHeight = '100px';
|
||||
img.style.objectFit = 'cover';
|
||||
currentThumbnailPreview.appendChild(img);
|
||||
} else {
|
||||
currentThumbnailPreview.innerHTML = '<small>No thumbnail set.</small>';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('editLinkForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
|
||||
// Append the remove_image flag if checked
|
||||
if (document.getElementById('removeCurrentThumbnail').checked) {
|
||||
formData.append('remove_image', 'true');
|
||||
}
|
||||
|
||||
// The action and link_id are already in the form as hidden inputs
|
||||
// No need to manually add csrf_token if it's already a hidden input in the form
|
||||
|
||||
// sendAction now uses FormData directly
|
||||
sendAction(formData);
|
||||
});
|
||||
|
||||
function sendAction(formData) {
|
||||
fetch('links.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('DEBUG POST Data:', data.debug_post);
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
window.location.reload(); // Simple reload for now, can be optimized later
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch error:', error);
|
||||
alert('An error occurred while processing your request.');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,154 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/../includes/Settings.php';
|
||||
|
||||
$allSettings = Settings::getAllWithMetadata();
|
||||
|
||||
// Handle form submission to update settings
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Protection (consider adding this if not already in auth.php or a global handler)
|
||||
// if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
// // Handle CSRF token mismatch
|
||||
// $_SESSION['error_message'] = 'CSRF token validation failed.';
|
||||
// header('Location: settings.php');
|
||||
// exit;
|
||||
// }
|
||||
|
||||
foreach ($allSettings as $setting) {
|
||||
$key = $setting['setting_key'];
|
||||
$newValue = $_POST[$key] ?? null;
|
||||
|
||||
// Special handling for checkboxes: if not present in POST, it means unchecked
|
||||
if ($setting['setting_type'] === 'checkbox') {
|
||||
$newValue = isset($_POST[$key]) ? '1' : '0';
|
||||
}
|
||||
|
||||
// Update the setting using the Settings class
|
||||
Settings::set(
|
||||
$key,
|
||||
$newValue,
|
||||
$setting['setting_type'],
|
||||
$setting['default_value'],
|
||||
$setting['validation_rules'],
|
||||
$setting['description']
|
||||
);
|
||||
}
|
||||
Settings::clearCache(); // Clear cache after updating settings
|
||||
$_SESSION['success_message'] = 'Settings updated successfully.';
|
||||
header('Location: settings.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Re-fetch settings after potential update
|
||||
$allSettings = Settings::getAllWithMetadata();
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage Settings - Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="../index.php">View Site</a>
|
||||
<a href="../logout.php">Logout</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<aside class="admin-nav">
|
||||
<h3>Menu</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link" href="index.php">Dashboard</a>
|
||||
<a class="nav-link" href="categories.php">Categories</a>
|
||||
<a class="nav-link" href="users.php">Users</a>
|
||||
<a class="nav-link" href="links.php">Links</a>
|
||||
<a class="nav-link active" href="settings.php">Settings</a>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<main class="content">
|
||||
<h2>Manage Settings</h2>
|
||||
|
||||
<?php if (isset($_SESSION['success_message'])): ?>
|
||||
<div class="alert alert-success">
|
||||
<?php echo $_SESSION['success_message']; unset($_SESSION['success_message']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION['error_message'])): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php echo $_SESSION['error_message']; unset($_SESSION['error_message']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="settings.php" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?? ''; // Assuming CSRF token is available globally or generated here ?>">
|
||||
<?php foreach ($allSettings as $setting): ?>
|
||||
<div class="mb-3">
|
||||
<label for="<?php echo htmlspecialchars($setting['setting_key']); ?>" class="form-label">
|
||||
<?php echo htmlspecialchars($setting['description'] ?: ucwords(str_replace('_', ' ', $setting['setting_key']))); ?>
|
||||
</label>
|
||||
<?php
|
||||
$currentValue = Settings::get($setting['setting_key'], $setting['default_value']);
|
||||
switch ($setting['setting_type']) {
|
||||
case 'text':
|
||||
case 'string':
|
||||
echo '<input type="text" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
|
||||
break;
|
||||
case 'number':
|
||||
case 'integer':
|
||||
echo '<input type="number" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
|
||||
break;
|
||||
case 'textarea':
|
||||
echo '<textarea class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" rows="3">' . htmlspecialchars($currentValue) . '</textarea>';
|
||||
break;
|
||||
case 'checkbox':
|
||||
case 'boolean':
|
||||
echo '<div class="form-check form-switch">';
|
||||
echo '<input class="form-check-input" type="checkbox" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="1" ' . ($currentValue ? 'checked' : '') . '>';
|
||||
echo '<label class="form-check-label" for="' . htmlspecialchars($setting['setting_key']) . '"></label>';
|
||||
echo '</div>';
|
||||
break;
|
||||
case 'dropdown':
|
||||
case 'select':
|
||||
$options = json_decode($setting['validation_rules'], true)['options'] ?? []; // Assuming options are stored in validation_rules as JSON
|
||||
echo '<select class="form-select" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '>';
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
echo '<option value="' . htmlspecialchars($optionValue) . '" ' . ($currentValue == $optionValue ? 'selected' : '') . '>' . htmlspecialchars($optionLabel) . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
break;
|
||||
// Add more types as needed (e.g., color, date, email)
|
||||
default:
|
||||
echo '<input type="text" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<?php if (!empty($setting['description'])): ?>
|
||||
<div class="form-text"><?php echo htmlspecialchars($setting['description']); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
101
admin/users.php
101
admin/users.php
@ -1,101 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
$pdo = db();
|
||||
|
||||
// Handle user role updates
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_role'])) {
|
||||
$user_id = $_POST['user_id'];
|
||||
$role = $_POST['role'];
|
||||
// Add extra validation for role value
|
||||
if (in_array($role, ['regular', 'power_user', 'admin'])) {
|
||||
$stmt = $pdo->prepare("UPDATE users SET role = ? WHERE id = ?");
|
||||
$stmt->execute([$role, $user_id]);
|
||||
}
|
||||
header("Location: users.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$users = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY created_at DESC")->fetchAll();
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage Users - Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="../index.php">View Site</a>
|
||||
<a href="../logout.php">Logout</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<aside class="admin-nav">
|
||||
<h3>Menu</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link" href="index.php">Dashboard</a>
|
||||
<a class="nav-link" href="categories.php">Categories</a>
|
||||
<a class="nav-link active" href="users.php">Users</a>
|
||||
<a class="nav-link" href="links.php">Links</a>
|
||||
<a class="nav-link" href="settings.php">Settings</a>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<main class="content">
|
||||
<h2>Manage Users</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Registered</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($user['username']); ?></td>
|
||||
<td>
|
||||
<form method="POST" action="users.php" class="d-inline">
|
||||
<input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
|
||||
<select name="role" class="form-select form-select-sm" style="width: auto; display: inline-block;">
|
||||
<option value="regular" <?php echo ($user['role'] === 'regular') ? 'selected' : ''; ?>>Regular</option>
|
||||
<option value="power_user" <?php echo ($user['role'] === 'power_user') ? 'selected' : ''; ?>>Power User</option>
|
||||
<option value="admin" <?php echo ($user['role'] === 'admin') ? 'selected' : ''; ?>>Admin</option>
|
||||
</select>
|
||||
<button type="submit" name="update_role" class="btn btn-sm btn-primary">Update</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><?php echo date("Y-m-d", strtotime($user['created_at'])); ?></td>
|
||||
<td>
|
||||
<!-- Future actions like delete or view profile -->
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,253 +0,0 @@
|
||||
/* --- Modern Japanese Retro Theme with more Pizazz --- */
|
||||
|
||||
:root {
|
||||
--primary-color: #007bff; /* Vibrant Blue */
|
||||
--secondary-color: #ff4081; /* Pink Accent */
|
||||
--tertiary-color: #f0f2f5; /* Light Gray Background */
|
||||
--text-dark: #212529;
|
||||
--text-medium: #495057;
|
||||
--text-light: #ced4da;
|
||||
--border-color: #dee2e6;
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
--white: #ffffff;
|
||||
--gradient-start: #e0f2f7; /* Light blue for gradient */
|
||||
--gradient-end: #f0f8ff; /* Lighter blue for gradient */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Noto Sans JP', sans-serif;
|
||||
background: linear-gradient(to bottom right, var(--gradient-start), var(--gradient-end));
|
||||
color: var(--text-dark);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--white);
|
||||
color: var(--primary-color);
|
||||
padding: 10px 25px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
box-shadow: 0 2px 8px var(--shadow-light);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-family: 'Zen Old Mincho', serif;
|
||||
font-size: 2.5em;
|
||||
color: var(--primary-color);
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.auth-links a {
|
||||
margin-left: 20px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-links a:hover {
|
||||
color: var(--secondary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
padding-top: 25px; /* Increased space below the header */
|
||||
}
|
||||
|
||||
.content-section {
|
||||
background-color: var(--white);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 30px; /* Increased space between sections */
|
||||
box-shadow: 0 4px 15px var(--shadow-light);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
background-color: var(--white);
|
||||
padding: 25px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.category-list h3 {
|
||||
font-family: 'Zen Old Mincho', serif;
|
||||
font-size: 1.7rem;
|
||||
color: var(--primary-color);
|
||||
border-bottom: 3px solid var(--secondary-color);
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-list .nav-link {
|
||||
color: var(--text-dark);
|
||||
padding: 8px 0;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.category-list .nav-link:hover {
|
||||
color: var(--secondary-color);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 25px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
font-family: 'Zen Old Mincho', serif;
|
||||
color: var(--primary-color);
|
||||
font-size: 2rem;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.link-item {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--white);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.link-item .thumbnail {
|
||||
width: 140px;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
|
||||
border-radius: 4px;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-item-body {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.link-item-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.link-item-title a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.link-item-title a:hover {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link-item-url {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-medium);
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.link-item-description {
|
||||
font-size: 1rem;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 25px 0;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 40px;
|
||||
box-shadow: 0 -2px 8px var(--shadow-light);
|
||||
}
|
||||
|
||||
/* Featured Section Styles */
|
||||
.featured-section {
|
||||
background: linear-gradient(to bottom, #fff5e6, #ffe0b3); /* Warm gradient background */
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 15px rgba(255, 160, 0, 0.1);
|
||||
border: 1px solid #ffcc80;
|
||||
}
|
||||
|
||||
.featured-section h3 {
|
||||
font-family: 'Zen Old Mincho', serif;
|
||||
color: #e65100; /* Darker orange for heading */
|
||||
font-size: 1.6rem;
|
||||
border-bottom: 3px solid #ff9800; /* Orange underline */
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.featured-item {
|
||||
background-color: var(--white);
|
||||
border: 1px solid #ffecb3;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.featured-item:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.featured-item h4 {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.featured-item p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-medium);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.featured-item .btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 8px 15px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.featured-item .btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.featured-item .btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.featured-item .btn-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.featured-item .btn-secondary:hover {
|
||||
background-color: #c00c4e;
|
||||
border-color: #c00c4e;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@ -1 +0,0 @@
|
||||
// Future javascript for interactivity
|
||||
@ -1,58 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
echo "Applying migrations...
|
||||
";
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Create migrations table if it doesn't exist
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS `migrations` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`migration_name` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`applied_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
");
|
||||
|
||||
$migrationsDir = __DIR__ . '/migrations/';
|
||||
$migrationFiles = glob($migrationsDir . '*.sql');
|
||||
sort($migrationFiles);
|
||||
|
||||
foreach ($migrationFiles as $file) {
|
||||
$migrationName = basename($file);
|
||||
|
||||
// Check if migration has already been applied
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM `migrations` WHERE `migration_name` = ?");
|
||||
$stmt->execute([$migrationName]);
|
||||
if ($stmt->fetchColumn() > 0) {
|
||||
echo "Skipping already applied migration: $migrationName
|
||||
";
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "Applying migration: $migrationName
|
||||
";
|
||||
$sql = file_get_contents($file);
|
||||
$pdo->exec($sql);
|
||||
|
||||
// Record the applied migration
|
||||
$stmt = $pdo->prepare("INSERT INTO `migrations` (`migration_name`) VALUES (?)");
|
||||
$stmt->execute([$migrationName]);
|
||||
echo "Successfully applied migration: $migrationName
|
||||
";
|
||||
}
|
||||
|
||||
echo "All migrations applied.
|
||||
";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "Database error: " . $e->getMessage() . "
|
||||
";
|
||||
exit(1);
|
||||
} catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "
|
||||
";
|
||||
exit(1);
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`password` VARCHAR(255) NOT NULL,
|
||||
`role` ENUM('regular', 'power_user', 'admin') NOT NULL DEFAULT 'regular',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `categories` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`visibility` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
`display_order` INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `subcategories` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`category_id` INT NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `links` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` INT NOT NULL,
|
||||
`subcategory_id` INT NOT NULL,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`url` VARCHAR(2083) NOT NULL,
|
||||
`description` TEXT,
|
||||
`thumbnail_url` VARCHAR(2083),
|
||||
`status` ENUM('pending', 'approved', 'rejected') NOT NULL DEFAULT 'pending',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
|
||||
FOREIGN KEY (`subcategory_id`) REFERENCES `subcategories`(`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `moderation_logs` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`link_id` INT NOT NULL,
|
||||
`moderator_id` INT NOT NULL,
|
||||
`action` ENUM('approved', 'rejected') NOT NULL,
|
||||
`notes` TEXT,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`link_id`) REFERENCES `links`(`id`),
|
||||
FOREIGN KEY (`moderator_id`) REFERENCES `users`(`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `visits` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`link_id` INT,
|
||||
`user_id` INT,
|
||||
`ip_address` VARCHAR(45),
|
||||
`user_agent` TEXT,
|
||||
`visited_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`link_id`) REFERENCES `links`(`id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
|
||||
);
|
||||
@ -1 +0,0 @@
|
||||
ALTER TABLE `links` MODIFY COLUMN `status` ENUM('pending', 'approved', 'rejected', 'paused') NOT NULL DEFAULT 'pending';
|
||||
@ -1,13 +0,0 @@
|
||||
-- db/migrations/003_create_settings_table.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
setting_key VARCHAR(255) NOT NULL UNIQUE,
|
||||
setting_value TEXT,
|
||||
setting_type VARCHAR(50) NOT NULL DEFAULT 'text',
|
||||
default_value TEXT NULL,
|
||||
validation_rules TEXT NULL,
|
||||
description TEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
@ -1,10 +0,0 @@
|
||||
-- db/migrations/004_add_initial_settings.sql
|
||||
|
||||
-- Insert initial settings if they do not already exist
|
||||
INSERT IGNORE INTO settings (setting_key, setting_value, setting_type, default_value, description)
|
||||
VALUES
|
||||
('site_title', 'My Web Directory', 'text', 'My Web Directory', 'The main title of the website.'),
|
||||
('maintenance_mode', '0', 'checkbox', '0', 'Enable or disable maintenance mode for the site.'),
|
||||
('items_per_page', '10', 'number', '10', 'Number of items to display per page in listings.'),
|
||||
('admin_email', 'admin@example.com', 'text', 'admin@example.com', 'The email address for administrative notifications.'),
|
||||
('tar_pit_delay', '0', 'number', '0', 'Delay (in seconds) for tar pit defense against brute-force attacks.');
|
||||
230
export.mysql
230
export.mysql
@ -1,230 +0,0 @@
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: 127.0.0.1 Database: app_37018
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.11.14-MariaDB-0+deb12u2
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `categories`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `categories`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `categories` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`visibility` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`display_order` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `categories`
|
||||
--
|
||||
|
||||
LOCK TABLES `categories` WRITE;
|
||||
/*!40000 ALTER TABLE `categories` DISABLE KEYS */;
|
||||
INSERT INTO `categories` VALUES
|
||||
(1,'Arts & Entertainment',1,0),
|
||||
(2,'Business & Economy',1,0),
|
||||
(3,'Computers & Internet',1,0),
|
||||
(4,'Education',1,0),
|
||||
(5,'Government',1,0),
|
||||
(6,'Health & Fitness',1,0),
|
||||
(7,'Home & Garden',1,0),
|
||||
(8,'News & Media',1,0),
|
||||
(9,'Recreation & Sports',1,0),
|
||||
(10,'Reference',1,0),
|
||||
(11,'Science & Technology',1,0),
|
||||
(12,'Shopping',1,0),
|
||||
(13,'Society & Culture',1,0),
|
||||
(14,'Travel & Tourism',1,0),
|
||||
(15,'Cars & Vehicles',1,0),
|
||||
(16,'Food & Drink',1,0),
|
||||
(17,'Law & Legal Issues',1,0),
|
||||
(18,'Pets & Animals',1,0),
|
||||
(19,'Real Estate',1,0),
|
||||
(20,'Games',1,0);
|
||||
/*!40000 ALTER TABLE `categories` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `links`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `links`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `links` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`subcategory_id` int(11) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`url` varchar(2083) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`thumbnail_url` varchar(2083) DEFAULT NULL,
|
||||
`status` enum('pending','approved','rejected') NOT NULL DEFAULT 'pending',
|
||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `subcategory_id` (`subcategory_id`),
|
||||
CONSTRAINT `links_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `links_ibfk_2` FOREIGN KEY (`subcategory_id`) REFERENCES `subcategories` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `links`
|
||||
--
|
||||
|
||||
LOCK TABLES `links` WRITE;
|
||||
/*!40000 ALTER TABLE `links` DISABLE KEYS */;
|
||||
INSERT INTO `links` VALUES
|
||||
(1,1,1,'test title','https://title.com','this is a test description',NULL,'approved','2025-12-17 15:34:06');
|
||||
/*!40000 ALTER TABLE `links` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `moderation_logs`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `moderation_logs`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `moderation_logs` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`link_id` int(11) NOT NULL,
|
||||
`moderator_id` int(11) NOT NULL,
|
||||
`action` enum('approved','rejected') NOT NULL,
|
||||
`notes` text DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `link_id` (`link_id`),
|
||||
KEY `moderator_id` (`moderator_id`),
|
||||
CONSTRAINT `moderation_logs_ibfk_1` FOREIGN KEY (`link_id`) REFERENCES `links` (`id`),
|
||||
CONSTRAINT `moderation_logs_ibfk_2` FOREIGN KEY (`moderator_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `moderation_logs`
|
||||
--
|
||||
|
||||
LOCK TABLES `moderation_logs` WRITE;
|
||||
/*!40000 ALTER TABLE `moderation_logs` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `moderation_logs` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `subcategories`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `subcategories`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `subcategories` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`category_id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `category_id` (`category_id`),
|
||||
CONSTRAINT `subcategories_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `subcategories`
|
||||
--
|
||||
|
||||
LOCK TABLES `subcategories` WRITE;
|
||||
/*!40000 ALTER TABLE `subcategories` DISABLE KEYS */;
|
||||
INSERT INTO `subcategories` VALUES
|
||||
(1,1,'test Arts & Entertainment'),
|
||||
(2,11,'Science & Technology syb');
|
||||
/*!40000 ALTER TABLE `subcategories` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`role` enum('regular','power_user','admin') NOT NULL DEFAULT 'regular',
|
||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `users`
|
||||
--
|
||||
|
||||
LOCK TABLES `users` WRITE;
|
||||
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
|
||||
INSERT INTO `users` VALUES
|
||||
(1,'admin','$2y$10$ZXgcZZeRqeZmt3gD1hqnVedgdgGwQ4R5dFoY6YRT.GY0StKYwnx5.','admin','2025-12-17 15:27:26');
|
||||
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `visits`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `visits`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `visits` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`link_id` int(11) DEFAULT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`ip_address` varchar(45) DEFAULT NULL,
|
||||
`user_agent` text DEFAULT NULL,
|
||||
`visited_at` timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `link_id` (`link_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `visits_ibfk_1` FOREIGN KEY (`link_id`) REFERENCES `links` (`id`),
|
||||
CONSTRAINT `visits_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `visits`
|
||||
--
|
||||
|
||||
LOCK TABLES `visits` WRITE;
|
||||
/*!40000 ALTER TABLE `visits` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `visits` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-12-17 23:45:53
|
||||
@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ImageProcessor {
|
||||
private static $uploadDir = 'assets/images/uploads/';
|
||||
private static $maxWidth = 400; // Default max width for thumbnails
|
||||
private static $maxHeight = 800; // Default max height for thumbnails (double the max width)
|
||||
|
||||
/**
|
||||
* Handles the uploaded image, moves it to the upload directory, and resizes it.
|
||||
*
|
||||
* @param array $file The $_FILES array entry for the uploaded file.
|
||||
* @return string|false The path to the saved thumbnail or false on error.
|
||||
*/
|
||||
public static function processAndSaveImage(array $file) {
|
||||
if (!isset($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) {
|
||||
error_log("ImageProcessor: No file uploaded or an error occurred. File error code: " . ($file['error'] ?? 'unknown'));
|
||||
return false; // No file uploaded or an error occurred
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
if (!in_array($file['type'], $allowedTypes)) {
|
||||
error_log("ImageProcessor: Invalid file type uploaded: " . $file['type']);
|
||||
return false; // Invalid file type
|
||||
}
|
||||
|
||||
// Ensure upload directory exists
|
||||
if (!is_dir(self::$uploadDir)) {
|
||||
mkdir(self::$uploadDir, 0775, true);
|
||||
}
|
||||
|
||||
// Generate a unique file name
|
||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$fileName = uniqid('thumbnail_') . '.' . $extension;
|
||||
$targetPath = self::$uploadDir . $fileName;
|
||||
|
||||
// Move the uploaded file
|
||||
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||
error_log("ImageProcessor: Failed to move uploaded file from " . $file['tmp_name'] . " to " . $targetPath);
|
||||
return false; // Failed to move uploaded file
|
||||
}
|
||||
|
||||
// Resize image
|
||||
if (!self::resizeImage($targetPath, $file['type'])) {
|
||||
error_log("ImageProcessor: Failed to resize image: " . $targetPath);
|
||||
// If resizing fails, you might want to delete the original uploaded file
|
||||
unlink($targetPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $targetPath; // Return the path to the saved and resized image
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes an image to a maximum width, maintaining aspect ratio.
|
||||
*
|
||||
* @param string $imagePath The path to the image file.
|
||||
* @param string $mimeType The MIME type of the image.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
private static function resizeImage(string $imagePath, string $mimeType) {
|
||||
list($width, $height) = getimagesize($imagePath);
|
||||
|
||||
if ($width <= self::$maxWidth && $height <= self::$maxHeight) {
|
||||
return true; // No resizing needed
|
||||
}
|
||||
|
||||
$aspectRatio = $width / $height;
|
||||
$newWidth = $width;
|
||||
$newHeight = $height;
|
||||
|
||||
// Resize based on width if it exceeds maxWidth or if new height based on maxWidth is less than maxHeight
|
||||
if ($newWidth > self::$maxWidth) {
|
||||
$newWidth = self::$maxWidth;
|
||||
$newHeight = (int) (self::$maxWidth / $aspectRatio);
|
||||
}
|
||||
|
||||
// Resize based on height if it exceeds maxHeight or if new width based on maxHeight is less than maxWidth
|
||||
if ($newHeight > self::$maxHeight) {
|
||||
$newHeight = self::$maxHeight;
|
||||
$newWidth = (int) (self::$maxHeight * $aspectRatio);
|
||||
}
|
||||
|
||||
// Final check to ensure it fits within both dimensions after initial adjustments
|
||||
if ($newWidth > self::$maxWidth) {
|
||||
$newWidth = self::$maxWidth;
|
||||
$newHeight = (int) (self::$maxWidth / $aspectRatio);
|
||||
}
|
||||
|
||||
if ($newHeight > self::$maxHeight) {
|
||||
$newHeight = self::$maxHeight;
|
||||
$newWidth = (int) (self::$maxHeight * $aspectRatio);
|
||||
}
|
||||
|
||||
|
||||
$image = null;
|
||||
switch ($mimeType) {
|
||||
case 'image/jpeg':
|
||||
$image = imagecreatefromjpeg($imagePath);
|
||||
break;
|
||||
case 'image/png':
|
||||
$image = imagecreatefrompng($imagePath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$image = imagecreatefromgif($imagePath);
|
||||
break;
|
||||
default:
|
||||
error_log("ImageProcessor: Unsupported image type for resizing: " . $mimeType);
|
||||
return false; // Unsupported image type for resizing
|
||||
}
|
||||
|
||||
if (!$image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$resizedImage = imagecreatetruecolor($newWidth, $newHeight);
|
||||
|
||||
// Preserve transparency for PNG and GIF
|
||||
if ($mimeType == 'image/png') {
|
||||
imagealphablending($resizedImage, false);
|
||||
imagesavealpha($resizedImage, true);
|
||||
$transparent = imagecolorallocatealpha($resizedImage, 255, 255, 255, 127);
|
||||
imagefilledrectangle($resizedImage, 0, 0, $newWidth, $newHeight, $transparent);
|
||||
} elseif ($mimeType == 'image/gif') {
|
||||
$trnprt_indx = imagecolortransparent($image);
|
||||
if ($trnprt_indx >= 0) {
|
||||
// Get the transparent color from the original image
|
||||
$trnprt_color = imagecolorsforindex($image, $trnprt_indx);
|
||||
// Allocate the same color in the new image
|
||||
$trnprt_indx = imagecolorallocate($resizedImage, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue']);
|
||||
imagefill($resizedImage, 0, 0, $trnprt_indx);
|
||||
imagecolortransparent($resizedImage, $trnprt_indx);
|
||||
}
|
||||
}
|
||||
|
||||
imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
|
||||
|
||||
$success = false;
|
||||
switch ($mimeType) {
|
||||
case 'image/jpeg':
|
||||
$success = imagejpeg($resizedImage, $imagePath, 90); // 90% quality
|
||||
break;
|
||||
case 'image/png':
|
||||
$success = imagepng($resizedImage, $imagePath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$success = imagegif($resizedImage, $imagePath);
|
||||
break;
|
||||
}
|
||||
|
||||
imagedestroy($image);
|
||||
imagedestroy($resizedImage);
|
||||
|
||||
return $success;
|
||||
}
|
||||
}
|
||||
@ -1,178 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
class Settings
|
||||
{
|
||||
private static $cache = [];
|
||||
|
||||
/**
|
||||
* Get a setting value.
|
||||
*
|
||||
* @param string $key The setting key.
|
||||
* @param mixed $default The default value to return if the setting is not found.
|
||||
* @return mixed The setting value or the default value.
|
||||
*/
|
||||
public static function get(string $key, $default = null)
|
||||
{
|
||||
if (isset(self::$cache[$key])) {
|
||||
return self::$cache[$key];
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT setting_value, setting_type FROM settings WHERE setting_key = ?");
|
||||
$stmt->execute([$key]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row) {
|
||||
$value = self::castValue($row['setting_value'], $row['setting_type']);
|
||||
self::$cache[$key] = $value;
|
||||
return $value;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting setting '{$key}': " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update a setting value.
|
||||
*
|
||||
* @param string $key The setting key.
|
||||
* @param mixed $value The setting value.
|
||||
* @param string $type The type of the setting (e.g., 'text', 'checkbox', 'number').
|
||||
* @param mixed $defaultValue The default value for the setting.
|
||||
* @param string|null $validationRules JSON string of validation rules.
|
||||
* @param string|null $description A description for the setting.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public static function set(
|
||||
string $key,
|
||||
$value,
|
||||
string $type = 'text',
|
||||
$defaultValue = null,
|
||||
?string $validationRules = null,
|
||||
?string $description = null
|
||||
): bool {
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Check if setting exists
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM settings WHERE setting_key = ?");
|
||||
$stmt->execute([$key]);
|
||||
$exists = $stmt->fetchColumn();
|
||||
|
||||
$stringValue = self::valueToString($value);
|
||||
$defaultValue = self::valueToString($defaultValue);
|
||||
|
||||
if ($exists) {
|
||||
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ?, setting_type = ?, default_value = ?, validation_rules = ?, description = ?, updated_at = NOW() WHERE setting_key = ?");
|
||||
$stmt->execute([$stringValue, $type, $defaultValue, $validationRules, $description, $key]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_type, default_value, validation_rules, description) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$key, $stringValue, $type, $defaultValue, $validationRules, $description]);
|
||||
}
|
||||
|
||||
// Clear cache for this key
|
||||
unset(self::$cache[$key]);
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error setting '{$key}': " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts a string value to its appropriate PHP type based on the setting type.
|
||||
*
|
||||
* @param string $value The string value from the database.
|
||||
* @param string $type The declared type of the setting.
|
||||
* @return mixed The type-casted value.
|
||||
*/
|
||||
private static function castValue(string $value, string $type)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'boolean':
|
||||
case 'checkbox':
|
||||
return (bool)$value;
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return (int)$value;
|
||||
case 'float':
|
||||
return (float)$value;
|
||||
case 'json':
|
||||
return json_decode($value, true);
|
||||
case 'array': // Simple comma-separated array
|
||||
return explode(',', $value);
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PHP value to its string representation for storage.
|
||||
*
|
||||
* @param mixed $value The PHP value.
|
||||
* @return string The string representation.
|
||||
*/
|
||||
private static function valueToString($value): string
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
} elseif (is_array($value) || is_object($value)) {
|
||||
return json_encode($value);
|
||||
} else {
|
||||
return (string)$value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire settings cache.
|
||||
*/
|
||||
public static function clearCache(): void
|
||||
{
|
||||
self::$cache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all settings from the database.
|
||||
*
|
||||
* @return array An associative array of all settings.
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT setting_key, setting_value, setting_type FROM settings");
|
||||
$allSettings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$result = [];
|
||||
foreach ($allSettings as $setting) {
|
||||
$result[$setting['setting_key']] = self::castValue($setting['setting_value'], $setting['setting_type']);
|
||||
}
|
||||
return $result;
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting all settings: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all settings with their metadata from the database.
|
||||
*
|
||||
* @return array An associative array of all settings including metadata.
|
||||
*/
|
||||
public static function getAllWithMetadata(): array
|
||||
{
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT * FROM settings");
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error getting all settings with metadata: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
318
index.php
318
index.php
@ -1,182 +1,150 @@
|
||||
<?php session_start(); ?>
|
||||
<!DOCTYPE html>
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!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'] ?? 'Web Directory'); ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'A public web directory of curated links.'); ?>">
|
||||
|
||||
|
||||
<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=Noto+Sans+JP:wght@300;400;700&family=Zen+Old+Mincho:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?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>
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Fetch visible categories from the database
|
||||
$stmt = $pdo->query("SELECT * FROM categories WHERE visibility = 1 ORDER BY display_order ASC, name ASC");
|
||||
$categories = $stmt->fetchAll();
|
||||
|
||||
// Determine the current category
|
||||
$current_category_id = null;
|
||||
$current_category_name = 'All Categories';
|
||||
if (isset($_GET['category']) && filter_var($_GET['category'], FILTER_VALIDATE_INT)) {
|
||||
$category_id_from_get = (int)$_GET['category'];
|
||||
$stmt = $pdo->prepare("SELECT id, name FROM categories WHERE id = ? AND visibility = 1");
|
||||
$stmt->execute([$category_id_from_get]);
|
||||
$cat = $stmt->fetch();
|
||||
if ($cat) {
|
||||
$current_category_id = $cat['id'];
|
||||
$current_category_name = $cat['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch links for the current category
|
||||
$link_stmt = null;
|
||||
if ($current_category_id) {
|
||||
$link_stmt = $pdo->prepare(
|
||||
"SELECT l.*, s.name as subcategory_name FROM links l " .
|
||||
"JOIN subcategories s ON l.subcategory_id = s.id " .
|
||||
"WHERE s.category_id = ? AND l.status = 'approved' ORDER BY s.name ASC, l.created_at DESC"
|
||||
);
|
||||
$link_stmt->execute([$current_category_id]);
|
||||
} else {
|
||||
$link_stmt = $pdo->query(
|
||||
"SELECT l.*, s.name as subcategory_name, c.name as category_name " .
|
||||
"FROM links l " .
|
||||
"JOIN subcategories s ON l.subcategory_id = s.id " .
|
||||
"JOIN categories c ON s.category_id = c.id " .
|
||||
"WHERE l.status = 'approved' " .
|
||||
"ORDER BY c.name ASC, s.name ASC, l.created_at DESC"
|
||||
);
|
||||
}
|
||||
$current_links = $link_stmt->fetchAll();
|
||||
?>
|
||||
<pre style="background-color: #fdd; border: 1px solid #f99; padding: 10px; margin: 10px;">
|
||||
DEBUG INFORMATION:
|
||||
GET Parameters: <?php echo htmlspecialchars(json_encode($_GET, JSON_PRETTY_PRINT)); ?>
|
||||
Current Category ID: <?php echo htmlspecialchars($current_category_id ?? "NULL"); ?>
|
||||
Current Category Name: <?php echo htmlspecialchars($current_category_name ?? "Not Set"); ?>
|
||||
Current Links Count: <?php echo htmlspecialchars(isset($current_links) ? count($current_links) : 0); ?>
|
||||
Raw Query String: <?php echo htmlspecialchars($_SERVER['QUERY_STRING'] ?? 'Not Set'); ?>
|
||||
SQL Query for links: <?php echo htmlspecialchars($link_stmt->queryString ?? "SQL Query not set yet."); ?>
|
||||
</pre>
|
||||
|
||||
|
||||
|
||||
|
||||
?>
|
||||
|
||||
<header class="header">
|
||||
<h1><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></h1>
|
||||
<div class="auth-links">
|
||||
<?php if (isset($_SESSION['user_id'])): ?>
|
||||
<a href="submit.php">Submit Link</a>
|
||||
<?php if ($_SESSION['user_role'] === 'admin'): ?>
|
||||
<a href="admin/index.php">Admin Panel</a>
|
||||
<?php endif; ?>
|
||||
<a href="logout.php">Logout</a>
|
||||
<?php else: ?>
|
||||
<a href="register.php">Register</a>
|
||||
<a href="login.php">Login</a>
|
||||
<?php endif; ?>
|
||||
<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>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<div class="main-wrapper container my-4">
|
||||
<div class="row">
|
||||
<!-- Left Column - Categories -->
|
||||
<div class="col-md-3">
|
||||
<aside class="category-list">
|
||||
<h3>Categories</h3>
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link <?php echo ($current_category_id === null) ? 'fw-bold' : ''; ?>" href="index.php">All Categories</a>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<a class="nav-link <?php echo ($category['id'] === $current_category_id) ? 'fw-bold' : ''; ?>" href="?category=<?php echo $category['id']; ?>">
|
||||
<?php echo htmlspecialchars($category['name']); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Center Column - Main Content -->
|
||||
<div class="col-md-6">
|
||||
<main class="content">
|
||||
<h2><?php echo htmlspecialchars($current_category_name); ?></h2>
|
||||
|
||||
<?php if (empty($current_links)): ?>
|
||||
<p>No links found in this category yet.</p>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$current_category_for_display = null;
|
||||
$current_subcategory = null;
|
||||
foreach ($current_links as $link):
|
||||
if ($current_category_id === null && isset($link['category_name']) && $link['category_name'] !== $current_category_for_display) {
|
||||
$current_category_for_display = $link['category_name'];
|
||||
echo '<h3>' . htmlspecialchars($current_category_for_display) . '</h3>';
|
||||
$current_subcategory = null; // Reset subcategory when category changes
|
||||
}
|
||||
|
||||
if ($link['subcategory_name'] !== $current_subcategory) {
|
||||
$current_subcategory = $link['subcategory_name'];
|
||||
echo '<h4>' . htmlspecialchars($current_subcategory) . '</h4>';
|
||||
}
|
||||
?>
|
||||
<div class="link-item">
|
||||
<img src="<?php echo htmlspecialchars($link['thumbnail_url']); ?>" alt="Thumbnail for <?php echo htmlspecialchars($link['title']); ?>" class="thumbnail">
|
||||
<div class="link-item-body">
|
||||
<div class="link-item-title">
|
||||
<a href="<?php echo htmlspecialchars($link['url']); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo htmlspecialchars($link['title']); ?>
|
||||
</a>
|
||||
</div>
|
||||
<a href="<?php echo htmlspecialchars($link['url']); ?>" target="_blank" rel="noopener noreferrer" class="link-item-url"><?php echo htmlspecialchars($link['url']); ?></a>
|
||||
<p class="link-item-description">
|
||||
<?php echo htmlspecialchars($link['description']); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="col-md-3">
|
||||
<aside class="featured-section">
|
||||
<h3>Featured Content</h3>
|
||||
<div class="featured-item">
|
||||
<h4>Special Link 1</h4>
|
||||
<p>A description for a special featured link.</p>
|
||||
<a href="#" class="btn btn-sm btn-primary">View More</a>
|
||||
</div>
|
||||
<div class="featured-item mt-3">
|
||||
<h4>Announcement</h4>
|
||||
<p>Check out our latest updates!</p>
|
||||
<a href="#" class="btn btn-sm btn-secondary">Read Blog</a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /main-wrapper -->
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
105
login.php
105
login.php
@ -1,105 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$errors = [];
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($username)) {
|
||||
$errors[] = 'Username is required.';
|
||||
}
|
||||
if (empty($password)) {
|
||||
$errors[] = 'Password is required.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT id, username, password, role FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['user_role'] = $user['role'];
|
||||
// For admin panel security features
|
||||
if ($user['role'] === 'admin') {
|
||||
$_SESSION['admin_ip_address'] = $_SERVER['REMOTE_ADDR'];
|
||||
$_SESSION['last_activity'] = time();
|
||||
}
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
} else {
|
||||
$errors[] = 'Invalid username or password.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$errors[] = "Database error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="register.php">Register</a>
|
||||
<a href="login.php">Login</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<main class="content p-4">
|
||||
<h2>Login</h2>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<p class="mb-0"><?php echo $error; ?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="login.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,6 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
107
register.php
107
register.php
@ -1,107 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$errors = [];
|
||||
$success = false;
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password_confirm = $_POST['password_confirm'] ?? '';
|
||||
|
||||
if (empty($username)) {
|
||||
$errors[] = 'Username is required.';
|
||||
}
|
||||
if (empty($password)) {
|
||||
$errors[] = 'Password is required.';
|
||||
}
|
||||
if ($password !== $password_confirm) {
|
||||
$errors[] = 'Passwords do not match.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
if ($stmt->fetch()) {
|
||||
$errors[] = 'Username already taken.';
|
||||
} else {
|
||||
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
|
||||
// For now, all new users are 'regular'. The first admin will be created manually.
|
||||
$stmt->execute([$username, $hashed_password, 'regular']);
|
||||
$success = true;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$errors[] = "Database error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></a></h1>
|
||||
<div class="auth-links">
|
||||
<a href="register.php">Register</a>
|
||||
<a href="login.php">Login</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<main class="content p-4">
|
||||
<h2>Register</h2>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<p class="mb-0"><?php echo $error; ?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success">
|
||||
<p class="mb-0">Registration successful! You can now <a href="login.php">login</a>.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form action="register.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password_confirm" class="form-label">Confirm Password</label>
|
||||
<input type="password" class="form-control" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
186
submit.php
186
submit.php
@ -1,186 +0,0 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/ImageProcessor.php';
|
||||
|
||||
error_log("submit.php: Script started.");
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$errors = [];
|
||||
$success = false;
|
||||
|
||||
// Fetch categories and subcategories for the form
|
||||
$categories = $pdo->query("SELECT * FROM categories WHERE visibility = 1 ORDER BY display_order ASC, name ASC")->fetchAll();
|
||||
$subcategories = [];
|
||||
if (!empty($categories)) {
|
||||
$stmt = $pdo->query("SELECT * FROM subcategories ORDER BY name ASC");
|
||||
while ($row = $stmt->fetch()) {
|
||||
$subcategories[$row['category_id']][] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
error_log("submit.php: POST request received.");
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$subcategory_id = $_POST['subcategory_id'] ?? null;
|
||||
|
||||
if (empty($title)) $errors[] = 'Title is required.';
|
||||
if (empty($url)) $errors[] = 'URL is required.';
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) $errors[] = 'Invalid URL.';
|
||||
if (empty($subcategory_id)) $errors[] = 'Subcategory is required.';
|
||||
|
||||
if (empty($errors)) {
|
||||
// Determine status based on user role
|
||||
$status = ($_SESSION['user_role'] === 'admin' || $_SESSION['user_role'] === 'power_user') ? 'approved' : 'pending';
|
||||
|
||||
// For now, thumbnail is not implemented
|
||||
$thumbnail_url = null;
|
||||
|
||||
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
|
||||
error_log("submit.php: Image upload detected. Processing image...");
|
||||
$uploadedImagePath = ImageProcessor::processAndSaveImage($_FILES['thumbnail']);
|
||||
if ($uploadedImagePath) {
|
||||
$thumbnail_url = $uploadedImagePath;
|
||||
error_log("submit.php: Image processed successfully. Path: " . $uploadedImagePath);
|
||||
} else {
|
||||
$errors[] = 'Failed to process uploaded image. Please ensure it is a valid image file (JPEG, PNG, GIF).';
|
||||
error_log("submit.php: Failed to process uploaded image.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
error_log("submit.php: Attempting database insertion...");
|
||||
$stmt = $pdo->prepare("INSERT INTO links (user_id, subcategory_id, title, url, description, thumbnail_url, status) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$_SESSION['user_id'], $subcategory_id, $title, $url, $description, $thumbnail_url, $status]);
|
||||
$success = true;
|
||||
error_log("submit.php: Database insertion successful.");
|
||||
} catch (PDOException $e) {
|
||||
$errors[] = "Database error: " . $e->getMessage();
|
||||
error_log("submit.php: Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Submit a Link - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></a></h1>
|
||||
<div class="auth-links">
|
||||
<?php if (isset($_SESSION['user_id'])): ?>
|
||||
<a href="submit.php">Submit Link</a>
|
||||
<a href="logout.php">Logout</a>
|
||||
<?php else: ?>
|
||||
<a href="register.php">Register</a>
|
||||
<a href="login.php">Login</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<main class="content p-4">
|
||||
<h2>Submit a New Link</h2>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php foreach ($errors as $error): ?><p class="mb-0"><?php echo $error; ?></p><?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success">
|
||||
<p class="mb-0">Thank you for your submission! It will be reviewed shortly.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form action="submit.php" method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<input type="url" class="form-control" id="url" name="url" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="category" class="form-label">Category</label>
|
||||
<select class="form-select" id="category" name="category">
|
||||
<option selected disabled>-- Select a Category --</option>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<option value="<?php echo $cat['id']; ?>"><?php echo htmlspecialchars($cat['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subcategory_id" class="form-label">Subcategory</label>
|
||||
<select class="form-select" id="subcategory_id" name="subcategory_id" required>
|
||||
<option selected disabled>-- Select a Subcategory --</option>
|
||||
<?php foreach($subcategories as $cat_id => $subs): ?>
|
||||
<?php foreach($subs as $sub): ?>
|
||||
<option class="d-none" data-category="<?php echo $cat_id; ?>" value="<?php echo $sub['id']; ?>"><?php echo htmlspecialchars($sub['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="thumbnail" class="form-label">Image (optional)</label>
|
||||
<input type="file" class="form-control" id="thumbnail" name="thumbnail" accept="image/*">
|
||||
<button type="submit" class="btn btn-primary">Submit Link</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
document.getElementById('category').addEventListener('change', function() {
|
||||
const categoryId = this.value;
|
||||
const subcategorySelect = document.getElementById('subcategory_id');
|
||||
|
||||
// Reset and show the default option
|
||||
subcategorySelect.value = '-- Select a Subcategory --';
|
||||
|
||||
// Hide all subcategory options
|
||||
Array.from(subcategorySelect.options).forEach(opt => {
|
||||
if (opt.dataset.category) { // Skip the default disabled option
|
||||
opt.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Show subcategories for the selected category
|
||||
const relevantOptions = subcategorySelect.querySelectorAll(`[data-category="${categoryId}"]`);
|
||||
relevantOptions.forEach(opt => opt.classList.remove('d-none'));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user