initial commit
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 549 KiB |
|
After Width: | Height: | Size: 546 KiB |
|
After Width: | Height: | Size: 874 KiB |
|
After Width: | Height: | Size: 866 KiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 253 KiB |
PostGIS
Mobile
QGIS
MapBender
GeoServer
GeoNode
GeoNetwork
Novella
Solutions