initial commit

This commit is contained in:
AcuGIS 2025-11-10 20:47:54 +02:00
parent 06266e3e23
commit b2dec6d24a
271 changed files with 23673 additions and 0 deletions

17
.readthedocs.yaml Normal file
View File

@ -0,0 +1,17 @@
version: "2"
build:
os: "ubuntu-22.04"
tools:
python: "3.10"
python:
install:
- requirements: docs/requirements.txt
sphinx:
configuration: docs/conf.py
formats:
- epub
- pdf

160
admin/settings.php Normal file
View File

@ -0,0 +1,160 @@
<?php
// TODO: Protect this page with your admin guard.
require_once '../incl/Auth.php';
if (!isLoggedIn() || !isAdmin()) { header('Location: /login.php'); exit; }
require_once '../incl/const.php';
require_once '../incl/db.php';
require_once '../incl/Settings.php';
session_start();
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32));
$csrf = $_SESSION['csrf'];
$settings = new Settings($pdo, '../assets/brand', '/assets/brand');
$current = $settings->load();
$errors = [];
$notice = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
$errors[] = 'Invalid CSRF token.';
} else {
[$saved, $errs] = $settings->save($_POST, $_FILES);
$current = $saved;
$errors = $errs;
if (!$errors) $notice = 'Settings updated successfully.';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Settings · Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root { --brand-primary: <?= htmlspecialchars($current['primary_color']) ?>; }
.brand-swatch { width: 28px; height: 28px; border-radius: 8px; background: var(--brand-primary); border: 1px solid rgba(0,0,0,0.1); }
.preview-img { max-height: 56px; }
.navbar { background: white; border-bottom: 2px solid var(--brand-primary); }
</style>
</head>
<body class="bg-light">
<!-- Set header variables for the include -->
<?php
$headerTitle = 'Settings';
$headerSubtitle = 'Admin Settings';
$headerIcon = 'gear';
include '../incl/header.php';
?>
<div class="container py-4">
<div class="d-flex align-items-center mb-4">
<h1 class="h4 mb-0">Site Settings</h1>
</div>
<?php if ($errors): ?>
<div class="alert alert-danger"><ul class="mb-0">
<?php foreach ($errors as $e): ?><li><?= htmlspecialchars($e) ?></li><?php endforeach; ?>
</ul></div>
<?php endif; ?>
<?php if ($notice): ?>
<div class="alert alert-success"><?= htmlspecialchars($notice) ?></div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" class="card p-3 shadow-sm bg-white">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
<div class="mb-3">
<label class="form-label">Site name</label>
<input type="text" name="site_name" class="form-control" required
value="<?= htmlspecialchars($current['site_name']) ?>">
</div>
<div class="mb-3">
<label class="form-label d-flex justify-content-between">Primary color
<small class="text-muted">Hex (e.g., #10b981)</small>
</label>
<div class="input-group">
<span class="input-group-text">#</span>
<input type="text" name="primary_color" class="form-control"
value="<?= ltrim(htmlspecialchars($current['primary_color']), '#') ?>">
<input type="color" class="form-control form-control-color" style="max-width: 60px"
value="<?= htmlspecialchars($current['primary_color']) ?>"
oninput="syncHex(this)">
</div>
</div>
<div class="mb-3">
<label class="form-label">Footer text</label>
<input type="text" name="footer_text" class="form-control"
value="<?= htmlspecialchars($current['footer_text']) ?>">
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label d-flex justify-content-between">Logo (PNG/JPG/WebP/SVG)
<?php if ($current['logo_url']): ?>
<a href="<?= htmlspecialchars($current['logo_url']) ?>" target="_blank">View</a>
<?php endif; ?>
</label>
<input type="file" name="logo" class="form-control" accept=".png,.jpg,.jpeg,.webp,.svg">
<?php if ($current['logo_url']): ?>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="delete_logo" id="delete_logo" value="1">
<label class="form-check-label" for="delete_logo">Remove current logo</label>
</div>
<div class="mt-2">
<img src="<?= htmlspecialchars($current['logo_url']) ?>" class="preview-img" alt="Logo preview">
</div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label d-flex justify-content-between">Hero image (PNG/JPG/WebP/SVG)
<?php if ($current['hero_image']): ?>
<a href="<?= htmlspecialchars($current['hero_image']) ?>" target="_blank">View</a>
<?php endif; ?>
</label>
<input type="file" name="hero_image" class="form-control" accept=".png,.jpg,.jpeg,.webp,.svg">
<?php if ($current['hero_image']): ?>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="delete_hero" id="delete_hero" value="1">
<label class="form-check-label" for="delete_hero">Remove current hero image</label>
</div>
<div class="mt-2">
<img src="<?= htmlspecialchars($current['hero_image']) ?>" class="preview-img" alt="Hero preview">
</div>
<?php endif; ?>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button class="btn btn-primary" type="submit">
<i class="bi bi-check-circle"></i> Save settings
</button>
<a class="btn btn-outline-secondary" href="/login.php" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Preview login
</a>
<a class="btn btn-outline-primary" href="../index.php">
<i class="bi bi-house"></i> Back to Home
</a>
</div>
</form>
<p class="text-muted small mt-3">This updates the database. If <code>settings.php</code> exists, it is merged as fallback.</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function syncHex(colorInput) {
const hexInput = document.querySelector('input[name="primary_color"]');
hexInput.value = colorInput.value.replace(/^#/, '');
}
</script>
</body>
</html>

56
assets/brand/README.md Normal file
View File

@ -0,0 +1,56 @@
# Brand Assets Directory
This directory stores uploaded brand assets for the GeoLite application.
## Contents
This directory will contain:
- **Logo images**: Uploaded via the Settings page
- **Hero images**: Background images for the login page
## File Naming Convention
Files are automatically named with the following pattern:
```
{type}_{date}_{time}_{random}.{ext}
Examples:
- logo_20251020_095430_a3f2b9c1.png
- hero_20251020_095445_d7e8f2a4.jpg
```
## Supported Formats
- PNG (.png)
- JPEG (.jpg, .jpeg)
- WebP (.webp)
- SVG (.svg)
## Permissions
This directory must be writable by the web server to allow image uploads.
### Linux/Unix
```bash
chmod 775 assets/brand
chown www-data:www-data assets/brand # Adjust user/group as needed
```
### Windows
The directory should have write permissions for the IIS/Apache user account.
## Security
- Files are validated for proper image format before upload
- Filenames are generated with random components to prevent guessing
- Only image files are allowed (validated by MIME type)
## Management
Images are managed through the Admin Settings page:
- **Upload**: Use the file input fields for Logo or Hero Image
- **Remove**: Check the "Remove current" checkbox and save
- **View**: Click the "View" link next to the upload field
Old images are automatically deleted when replaced or when removed via the checkbox.

129
assets/js/theme-toggle.js Normal file
View File

@ -0,0 +1,129 @@
/**
* GeoLite Theme Toggle
* Provides light/dark theme switching with persistence
*/
(function () {
const storageKey = 'geolite-theme';
const toggleSelector = '[data-theme-toggle]';
const toggles = new Set();
let initialized = false;
function getMediaMatcher() {
if (typeof window === 'undefined' || !window.matchMedia) return null;
return window.matchMedia('(prefers-color-scheme: dark)');
}
function updateToggleAppearance(button, isDark) {
if (!button) return;
const icon = button.querySelector('.theme-toggle-icon');
const label = button.querySelector('.theme-toggle-label');
if (icon) {
icon.classList.toggle('bi-moon-stars', !isDark);
icon.classList.toggle('bi-brightness-high', isDark);
}
if (label) {
label.textContent = isDark ? 'Light' : 'Dark';
}
button.setAttribute('title', isDark ? 'Switch to light mode' : 'Switch to dark mode');
}
function applyTheme(theme, persist = true) {
if (!document.body) {
// Defer until body exists
document.addEventListener('DOMContentLoaded', () => applyTheme(theme, persist), { once: true });
return;
}
const isDark = theme === 'dark';
document.body.classList.toggle('dark-mode', isDark);
document.documentElement.setAttribute('data-theme', theme);
if (persist) {
try {
localStorage.setItem(storageKey, theme);
} catch (error) {
console.warn('Theme preference could not be saved:', error);
}
}
toggles.forEach((btn) => updateToggleAppearance(btn, isDark));
}
function currentTheme() {
return document.body && document.body.classList.contains('dark-mode') ? 'dark' : 'light';
}
function handleToggleClick(event) {
event.preventDefault();
const nextTheme = currentTheme() === 'dark' ? 'light' : 'dark';
applyTheme(nextTheme);
}
function registerToggle(button) {
if (!button || toggles.has(button)) return;
toggles.add(button);
button.addEventListener('click', handleToggleClick);
updateToggleAppearance(button, currentTheme() === 'dark');
}
function initToggles() {
document.querySelectorAll(toggleSelector).forEach(registerToggle);
}
function initTheme() {
if (initialized) return;
initialized = true;
let savedTheme = null;
try {
savedTheme = localStorage.getItem(storageKey);
} catch (error) {
console.warn('Unable to read saved theme preference:', error);
}
if (!savedTheme) {
const media = getMediaMatcher();
savedTheme = media && media.matches ? 'dark' : 'light';
}
applyTheme(savedTheme, Boolean(savedTheme));
const media = getMediaMatcher();
if (media) {
const handleChange = (event) => {
try {
if (localStorage.getItem(storageKey)) return;
} catch (error) {
// continue
}
applyTheme(event.matches ? 'dark' : 'light', false);
};
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', handleChange);
} else if (typeof media.addListener === 'function') {
media.addListener(handleChange);
}
}
initToggles();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTheme);
} else {
initTheme();
}
window.GeoliteTheme = {
applyTheme,
registerToggle,
refresh: initToggles,
};
})();

View File

421
categories.php Normal file
View File

@ -0,0 +1,421 @@
<?php
require_once 'incl/Auth.php';
require_once 'incl/Database.php';
// Check if user is logged in and is admin
if (!isLoggedIn() || !isAdmin()) {
header('Location: login.php');
exit;
}
// Load brand settings
require_once 'incl/const.php';
require_once 'incl/db.php';
require_once 'incl/Settings.php';
$brand = [
'site_name' => 'GeoLite',
'logo_url' => null,
'primary_color' => '#667eea',
'hero_image' => null,
'footer_text' => '© ' . date('Y') . ' GeoLite'
];
// Load settings from database
$settingsService = new Settings($pdo, 'assets/brand', 'assets/brand');
$brand = array_merge($brand, $settingsService->load());
$message = '';
$error = '';
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
switch ($_POST['action']) {
case 'create':
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$color = trim($_POST['color']);
$icon = trim($_POST['icon']);
if (empty($name)) {
$error = 'Category name is required.';
} else {
try {
createCategory($name, $description, $color, $icon);
$message = 'Category created successfully!';
} catch (Exception $e) {
$error = 'Error creating category: ' . $e->getMessage();
}
}
break;
case 'update':
$id = (int)$_POST['id'];
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$color = trim($_POST['color']);
$icon = trim($_POST['icon']);
if (empty($name)) {
$error = 'Category name is required.';
} else {
try {
if (updateCategory($id, $name, $description, $color, $icon)) {
$message = 'Category updated successfully!';
} else {
$error = 'Error updating category.';
}
} catch (Exception $e) {
$error = 'Error updating category: ' . $e->getMessage();
}
}
break;
case 'delete':
$id = (int)$_POST['id'];
try {
if (deleteCategory($id)) {
$message = 'Category deleted successfully!';
} else {
$error = 'Error deleting category.';
}
} catch (Exception $e) {
$error = 'Error deleting category: ' . $e->getMessage();
}
break;
}
}
}
// Get all categories
try {
$categories = getAllCategories();
} catch (Exception $e) {
$error = 'Error loading categories: ' . $e->getMessage();
$categories = [];
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Categories - <?= htmlspecialchars($brand['site_name']) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--brand-primary: <?= htmlspecialchars($brand['primary_color']) ?>;
}
.brand-header {
background: linear-gradient(135deg, var(--brand-primary) 0%, #667eea 100%);
color: white;
padding: 1rem 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.brand-logo {
width: 32px;
height: 32px;
border-radius: 8px;
}
.brand-bar {
height: 4px;
background: linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100%);
}
.category-card {
border: 1px solid #e9ecef;
border-radius: 12px;
transition: all 0.3s ease;
overflow: hidden;
}
.category-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.category-header {
padding: 1rem;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
}
.category-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.2rem;
}
.color-preview {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 0 0 1px #dee2e6;
}
.btn-category {
background: var(--brand-primary);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.btn-category:hover {
background: #5a67d8;
color: white;
transform: translateY(-1px);
}
.modal-header {
background: var(--brand-primary);
color: white;
}
.form-control:focus {
border-color: var(--brand-primary);
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-primary {
background: var(--brand-primary);
border-color: var(--brand-primary);
}
.btn-primary:hover {
background: #5a67d8;
border-color: #5a67d8;
}
.site-footer {
color: #6b7280;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(4px);
}
</style>
</head>
<body>
<!-- Set header variables for the include -->
<?php
$headerTitle = 'Categories';
$headerSubtitle = 'Manage categories for organizing your maps and content';
$headerIcon = 'tags';
include 'incl/header.php';
?>
<div class="container-fluid" style="max-width: 98%; padding: 20px;">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2><i class="bi bi-tags"></i> Categories</h2>
<p class="text-muted">Manage categories for organizing your maps and content</p>
</div>
<button class="btn btn-category" data-bs-toggle="modal" data-bs-target="#categoryModal">
<i class="bi bi-plus-circle"></i> Add Category
</button>
</div>
<?php if ($message): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> <?= htmlspecialchars($message) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle"></i> <?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<?php foreach ($categories as $category): ?>
<div class="col-md-6 col-lg-4 mb-4">
<div class="category-card">
<div class="category-header">
<div class="d-flex align-items-center">
<div class="category-icon me-3" style="background-color: <?= htmlspecialchars($category['color']) ?>">
<i class="<?= htmlspecialchars($category['icon']) ?>"></i>
</div>
<div class="flex-grow-1">
<h5 class="mb-1"><?= htmlspecialchars($category['name']) ?></h5>
<div class="d-flex align-items-center">
<div class="color-preview me-2" style="background-color: <?= htmlspecialchars($category['color']) ?>"></div>
<small class="text-muted"><?= htmlspecialchars($category['icon']) ?></small>
</div>
</div>
</div>
</div>
<div class="p-3">
<p class="text-muted mb-3"><?= htmlspecialchars($category['description']) ?></p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Created: <?= date('M j, Y', strtotime($category['created_at'])) ?>
</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editCategory(<?= htmlspecialchars(json_encode($category)) ?>)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteCategory(<?= $category['id'] ?>, '<?= htmlspecialchars($category['name']) ?>')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($categories)): ?>
<div class="text-center py-5">
<i class="bi bi-tags text-muted" style="font-size: 3rem;"></i>
<h4 class="text-muted mt-3">No categories found</h4>
<p class="text-muted">Create your first category to get started.</p>
<button class="btn btn-category" data-bs-toggle="modal" data-bs-target="#categoryModal">
<i class="bi bi-plus-circle"></i> Add Category
</button>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Category Modal -->
<div class="modal fade" id="categoryModal" tabindex="-1" aria-labelledby="categoryModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="categoryModalLabel">Add Category</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="categoryForm">
<div class="modal-body">
<input type="hidden" name="action" id="formAction" value="create">
<input type="hidden" name="id" id="categoryId">
<div class="mb-3">
<label for="name" class="form-label">Category Name *</label>
<input type="text" class="form-control" id="name" name="name" required>
</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="row">
<div class="col-md-6">
<div class="mb-3">
<label for="color" class="form-label">Color</label>
<input type="color" class="form-control form-control-color" id="color" name="color" value="#667eea">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="icon" class="form-label">Icon</label>
<select class="form-select" id="icon" name="icon">
<option value="bi-tag">Tag</option>
<option value="bi-building">Building</option>
<option value="bi-car-front">Transportation</option>
<option value="bi-droplet">Water</option>
<option value="bi-tree">Land Use</option>
<option value="bi-mountain">Elevation</option>
<option value="bi-people">Population</option>
<option value="bi-flower1">Environment</option>
<option value="bi-lightning">Utilities</option>
<option value="bi-hospital">Emergency</option>
<option value="bi-tree-fill">Recreation</option>
<option value="bi-shop">Economic</option>
<option value="bi-cloud-sun">Weather</option>
<option value="bi-gear">Infrastructure</option>
<option value="bi-diagram-2">Boundaries</option>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Category</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the category "<span id="deleteCategoryName"></span>"?</p>
<p class="text-muted">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" id="deleteCategoryId">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
<!-- Site Footer -->
<footer class="site-footer mt-5 py-4">
<div class="container text-center">
<p class="mb-0"><?= htmlspecialchars($brand['footer_text']) ?></p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function editCategory(category) {
document.getElementById('categoryModalLabel').textContent = 'Edit Category';
document.getElementById('formAction').value = 'update';
document.getElementById('categoryId').value = category.id;
document.getElementById('name').value = category.name;
document.getElementById('description').value = category.description || '';
document.getElementById('color').value = category.color;
document.getElementById('icon').value = category.icon;
new bootstrap.Modal(document.getElementById('categoryModal')).show();
}
function deleteCategory(id, name) {
document.getElementById('deleteCategoryId').value = id;
document.getElementById('deleteCategoryName').textContent = name;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
// Reset form when modal is hidden
document.getElementById('categoryModal').addEventListener('hidden.bs.modal', function () {
document.getElementById('categoryForm').reset();
document.getElementById('categoryModalLabel').textContent = 'Add Category';
document.getElementById('formAction').value = 'create';
document.getElementById('categoryId').value = '';
});
</script>
</body>
</html>

3420
dashboard_builder.php Normal file

File diff suppressed because it is too large Load Diff

309
dashboards.php Normal file
View File

@ -0,0 +1,309 @@
<?php
// Start output buffering to prevent header issues
ob_start();
// Include required files
require_once 'incl/const.php';
require_once 'incl/Database.php';
require_once 'incl/Auth.php';
// Require authentication
requireAuth();
// Handle delete request
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete') {
$dashboardId = intval($_POST['dashboard_id']);
try {
deleteDashboard($dashboardId);
ob_end_clean();
header('Location: dashboards.php?deleted=1');
exit;
} catch (Exception $e) {
$error = "Failed to delete dashboard.";
}
}
// Get all saved dashboards
try {
$dashboards = getAllDashboards();
} catch (Exception $e) {
$error = "Failed to connect to database. Please check your configuration.";
$dashboards = [];
}
$savedMessage = isset($_GET['saved']) ? "Dashboard saved successfully!" : null;
$deletedMessage = isset($_GET['deleted']) ? "Dashboard deleted successfully!" : null;
// Flush output buffer
ob_end_flush();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoLite - Saved Dashboards</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0;
color: #333;
font-weight: 700;
}
.header p {
margin: 10px 0 0 0;
color: #666;
}
.dashboard-card {
background: white;
border-radius: 15px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
height: 100%;
display: flex;
flex-direction: column;
}
.dashboard-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
}
.card-header-custom {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
color: white;
}
.card-header-custom h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
}
.card-body {
padding: 20px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.card-description {
color: #666;
margin-bottom: 15px;
flex-grow: 1;
}
.card-meta {
font-size: 0.85rem;
color: #999;
margin-top: auto;
padding-top: 15px;
border-top: 1px solid #eee;
}
.card-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-custom {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.empty-state {
background: white;
border-radius: 15px;
padding: 60px 20px;
text-align: center;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.empty-state i {
font-size: 4rem;
color: #ddd;
margin-bottom: 20px;
}
.empty-state h3 {
color: #666;
margin-bottom: 10px;
}
.empty-state p {
color: #999;
margin-bottom: 30px;
}
.create-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 12px 30px;
border-radius: 25px;
color: white;
font-weight: 600;
text-decoration: none;
display: inline-block;
transition: transform 0.2s ease;
margin-right: 10px;
}
.create-btn:hover {
transform: scale(1.05);
color: white;
}
.back-btn {
background: white;
border: 2px solid #667eea;
color: #667eea;
padding: 10px 25px;
border-radius: 25px;
font-weight: 600;
text-decoration: none;
display: inline-block;
transition: all 0.2s ease;
}
.back-btn:hover {
background: #667eea;
color: white;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1><i class="bi bi-speedometer2"></i> GeoLite Dashboard Library</h1>
<p>View and manage your saved dashboards</p>
</div>
<div class="d-flex gap-2 align-items-center">
<span class="text-muted">
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars(getCurrentUsername()); ?>
</span>
<a href="dashboard_builder.php" class="create-btn">
<i class="bi bi-plus-circle"></i> Create New Dashboard
</a>
<a href="index.php" class="back-btn">
<i class="bi bi-map"></i> View Maps
</a>
<a href="logout.php" class="btn btn-outline-danger">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>
</div>
</div>
<?php if ($savedMessage): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> <?php echo $savedMessage; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($deletedMessage): ?>
<div class="alert alert-info alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle"></i> <?php echo $deletedMessage; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle"></i> <?php echo htmlspecialchars($error); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (empty($dashboards)): ?>
<div class="empty-state">
<i class="bi bi-speedometer2"></i>
<h3>No Dashboards Yet</h3>
<p>Create your first dashboard to get started with GeoLite</p>
<a href="dashboard_builder.php" class="create-btn">
<i class="bi bi-plus-circle"></i> Create Your First Dashboard
</a>
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($dashboards as $dashboard): ?>
<div class="col-md-6 col-lg-4">
<div class="dashboard-card">
<div class="card-header-custom">
<h3><i class="bi bi-speedometer2"></i> <?php echo htmlspecialchars($dashboard['title']); ?></h3>
</div>
<div class="card-body">
<div class="card-description">
<?php if (!empty($dashboard['description'])): ?>
<?php echo htmlspecialchars($dashboard['description']); ?>
<?php else: ?>
<em style="color: #ccc;">No description provided</em>
<?php endif; ?>
</div>
<div class="card-meta">
<div><i class="bi bi-calendar3"></i> Created: <?php echo date('M j, Y g:i A', strtotime($dashboard['created_at'])); ?></div>
<?php if ($dashboard['updated_at'] != $dashboard['created_at']): ?>
<div><i class="bi bi-clock-history"></i> Updated: <?php echo date('M j, Y g:i A', strtotime($dashboard['updated_at'])); ?></div>
<?php endif; ?>
</div>
<div class="card-actions">
<a href="view_dashboard.php?id=<?php echo $dashboard['id']; ?>" class="btn btn-primary btn-custom" target="_blank">
<i class="bi bi-eye"></i> View
</a>
<a href="dashboard_builder.php?id=<?php echo $dashboard['id']; ?>" class="btn btn-warning btn-custom">
<i class="bi bi-pencil"></i> Edit
</a>
<button type="button" class="btn btn-danger btn-custom" onclick="confirmDelete(<?php echo $dashboard['id']; ?>, '<?php echo htmlspecialchars(addslashes($dashboard['title'])); ?>')">
<i class="bi bi-trash"></i> Delete
</button>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle text-danger"></i> Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the dashboard "<strong id="dashboardTitle"></strong>"?</p>
<p class="text-muted mb-0">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="dashboard_id" id="deleteDashboardId">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Delete Dashboard
</button>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function confirmDelete(dashboardId, dashboardTitle) {
document.getElementById('dashboardTitle').textContent = dashboardTitle;
document.getElementById('deleteDashboardId').value = dashboardId;
var modal = new bootstrap.Modal(document.getElementById('deleteModal'));
modal.show();
}
</script>
</body>
</html>

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/_static/GeoLite-Categories-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/_static/GeoLite-Categories-edit.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
docs/_static/GeoLite-Dashboard-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
docs/_static/GeoLite-Dashboard-10.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/_static/GeoLite-Dashboard-11.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/_static/GeoLite-Dashboard-13.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/_static/GeoLite-Dashboard-14.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
docs/_static/GeoLite-Dashboard-15.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/_static/GeoLite-Dashboard-16.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
docs/_static/GeoLite-Dashboard-17.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/_static/GeoLite-Dashboard-18.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/_static/GeoLite-Dashboard-19.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/_static/GeoLite-Dashboard-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/_static/GeoLite-Dashboard-20.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
docs/_static/GeoLite-Dashboard-21.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
docs/_static/GeoLite-Dashboard-22.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/_static/GeoLite-Dashboard-23.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
docs/_static/GeoLite-Dashboard-24.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
docs/_static/GeoLite-Dashboard-25.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/_static/GeoLite-Dashboard-26.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/_static/GeoLite-Dashboard-27.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/_static/GeoLite-Dashboard-28.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
docs/_static/GeoLite-Dashboard-29.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/_static/GeoLite-Dashboard-3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/_static/GeoLite-Dashboard-30.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/_static/GeoLite-Dashboard-31.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/_static/GeoLite-Dashboard-32.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/_static/GeoLite-Dashboard-33.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/_static/GeoLite-Dashboard-4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/_static/GeoLite-Dashboard-5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/_static/GeoLite-Dashboard-6.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/_static/GeoLite-Dashboard-7.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/_static/GeoLite-Dashboard-8.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
docs/_static/GeoLite-Dashboard-9.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
docs/_static/GeoLite-Dashboard.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
docs/_static/GeoLite-Document-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
docs/_static/GeoLite-Document-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/_static/GeoLite-Document-3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/_static/GeoLite-Document-4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
docs/_static/GeoLite-Document-5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/_static/GeoLite-GeoServer-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/_static/GeoLite-GeoServer.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/_static/GeoLite-Github-Filter.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
docs/_static/GeoLite-Github.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
docs/_static/GeoLite-Groups-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/_static/GeoLite-Groups-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/_static/GeoLite-Groups-3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/_static/GeoLite-HTML-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
docs/_static/GeoLite-HTML-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/_static/GeoLite-HTML-3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/_static/GeoLite-HTML-4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/_static/GeoLite-HTML-5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/_static/GeoLite-HTML-6.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/_static/GeoLite-HTML-7.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/_static/GeoLite-HTML-8.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/_static/GeoLite-Install-Screen.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
docs/_static/GeoLite-Permissions-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
docs/_static/GeoLite-Permissions-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/_static/GeoLite-Permissions-3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/_static/GeoLite-Settings-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/_static/GeoLite-Settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
docs/_static/GeoLite-Users-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
docs/_static/GeoLite-Users-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
docs/_static/GeoLite-Users-3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
docs/_static/GeoLite-Users-4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/_static/GeoLite-Users-5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/_static/add-user-button.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/_static/add-users.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

13
docs/_static/css/custom.css vendored Normal file
View File

@ -0,0 +1,13 @@
.wy-nav-content {
max-width: 1350px;
}
.wy-side-nav-search {
display: block;
width: 300px;
padding: .809em;
margin-bottom: .809em;
z-index: 200;
background-color: #fff;
text-align: center;
color: #fcfcfc;
}

14
docs/_static/custom.css vendored Normal file
View File

@ -0,0 +1,14 @@
.wy-nav-content {
max-width: 1350px;
}
.wy-side-nav-search {
display: block;
width: 300px;
padding: .809em;
margin-bottom: .809em;
z-index: 200;
background-color: #fff;
text-align: center;
color: #fcfcfc;
}

BIN
docs/_static/demo-bees.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

BIN
docs/_static/demo-chicago.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

BIN
docs/_static/demo-neighborhoods.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

BIN
docs/_static/demo-paris-1550.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

BIN
docs/_static/demo-wms1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

BIN
docs/_static/demo-wms2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 KiB

BIN
docs/_static/demosets.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
docs/_static/geolite-logo-docs.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/_static/geolite-logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
docs/_static/harvest-completed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/_static/harvest-datasets-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Some files were not shown because too many files have changed in this diff Show More