Added galery
This commit is contained in:
@@ -76,7 +76,7 @@
|
||||
</span>
|
||||
<span class="tree-label"><?php echo htmlspecialchars($table); ?></span>
|
||||
</div>
|
||||
<div class="tree-children">
|
||||
<div class="tree-children"<?php echo $is_current_table ? ' data-loaded="true"' : ''; ?>>
|
||||
<?php if ($is_current_table && !empty($columns)): ?>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<div class="tree-item">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<link rel="icon" href="data:;base64,=">
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
|
||||
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/gallery.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- wrapper, to center website -->
|
||||
@@ -27,6 +28,9 @@
|
||||
<li <?php if (View::checkForActiveController($filename, "directory")) { echo ' class="active" '; } ?> >
|
||||
<a href="<?php echo Config::get('URL'); ?>directory/index">Benutzer</a>
|
||||
</li>
|
||||
<li <?php if (View::checkForActiveController($filename, "gallery")) { echo ' class="active" '; } ?> >
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/index">Gallery</a>
|
||||
</li>
|
||||
<?php if (Session::userIsLoggedIn()) { ?>
|
||||
<li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> >
|
||||
<a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a>
|
||||
|
||||
51
application/view/gallery/edit.php
Normal file
51
application/view/gallery/edit.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<div class="gallery-container">
|
||||
<div class="gallery-header">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>" class="gallery-back">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
Back to Image
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-container">
|
||||
<h1>Edit Image</h1>
|
||||
|
||||
<div class="gallery-edit-preview">
|
||||
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/thumb"
|
||||
alt="<?php echo htmlspecialchars($this->image->title); ?>">
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $this->image->id; ?>" class="gallery-form">
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-label" for="title">Title</label>
|
||||
<input type="text" name="title" id="title" class="gallery-form-input"
|
||||
value="<?php echo htmlspecialchars($this->image->title); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-label" for="description">Description</label>
|
||||
<textarea name="description" id="description" class="gallery-form-textarea" rows="4"><?php echo htmlspecialchars($this->image->description); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-checkbox">
|
||||
<input type="checkbox" name="is_public" value="1" <?php echo $this->image->is_public ? 'checked' : ''; ?>>
|
||||
<span>Make this image public</span>
|
||||
</label>
|
||||
<small class="gallery-form-hint">Public images are visible to everyone in the gallery</small>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-actions">
|
||||
<button type="submit" name="submit_edit" class="gallery-btn gallery-btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
Save Changes
|
||||
</button>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>" class="gallery-btn gallery-btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
59
application/view/gallery/index.php
Normal file
59
application/view/gallery/index.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<div class="gallery-container">
|
||||
<div class="gallery-header">
|
||||
<h1>Gallery</h1>
|
||||
<div class="gallery-actions">
|
||||
<?php if (Session::userIsLoggedIn()): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-btn gallery-btn-secondary">My Images</a>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary">Upload Image</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($this->images)): ?>
|
||||
<div class="gallery-empty">
|
||||
<div class="gallery-empty-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>No images yet</h3>
|
||||
<p>Be the first to share an image!</p>
|
||||
<?php if (Session::userIsLoggedIn()): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary" style="margin-top: 16px;">Upload Image</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="gallery-grid">
|
||||
<?php foreach ($this->images as $image): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $image->id; ?>" class="gallery-item">
|
||||
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $image->id; ?>/thumb"
|
||||
alt="<?php echo htmlspecialchars($image->title); ?>"
|
||||
loading="lazy">
|
||||
<div class="gallery-item-overlay">
|
||||
<span class="gallery-item-title"><?php echo htmlspecialchars($image->title); ?></span>
|
||||
<span class="gallery-item-author">by <?php echo htmlspecialchars($image->user_name); ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$total_pages = ceil($this->total_images / $this->per_page);
|
||||
if ($total_pages > 1):
|
||||
?>
|
||||
<div class="gallery-pagination">
|
||||
<?php if ($this->current_page > 1): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/index/<?php echo $this->current_page - 1; ?>" class="gallery-btn gallery-btn-secondary">Previous</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="gallery-pagination-info">Page <?php echo $this->current_page; ?> of <?php echo $total_pages; ?></span>
|
||||
|
||||
<?php if ($this->current_page < $total_pages): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/index/<?php echo $this->current_page + 1; ?>" class="gallery-btn gallery-btn-secondary">Next</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
76
application/view/gallery/my.php
Normal file
76
application/view/gallery/my.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<div class="gallery-container">
|
||||
<div class="gallery-header">
|
||||
<h1>My Images</h1>
|
||||
<div class="gallery-actions">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/index" class="gallery-btn gallery-btn-secondary">Public Gallery</a>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary">Upload Image</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($this->images)): ?>
|
||||
<div class="gallery-empty">
|
||||
<div class="gallery-empty-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>No images uploaded</h3>
|
||||
<p>Upload your first image to get started.</p>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary" style="margin-top: 16px;">Upload Image</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="gallery-grid">
|
||||
<?php foreach ($this->images as $image): ?>
|
||||
<div class="gallery-item gallery-item-owned">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $image->id; ?>">
|
||||
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $image->id; ?>/thumb"
|
||||
alt="<?php echo htmlspecialchars($image->title); ?>"
|
||||
loading="lazy">
|
||||
</a>
|
||||
<div class="gallery-item-overlay">
|
||||
<span class="gallery-item-title"><?php echo htmlspecialchars($image->title); ?></span>
|
||||
<span class="gallery-item-visibility <?php echo $image->is_public ? 'public' : 'private'; ?>">
|
||||
<?php echo $image->is_public ? 'Public' : 'Private'; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gallery-item-actions">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $image->id; ?>" class="gallery-btn-icon" title="Edit">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/delete/<?php echo $image->id; ?>"
|
||||
class="gallery-btn-icon gallery-btn-danger"
|
||||
title="Delete"
|
||||
onclick="return confirm('Delete this image?');">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$total_pages = ceil($this->total_images / $this->per_page);
|
||||
if ($total_pages > 1):
|
||||
?>
|
||||
<div class="gallery-pagination">
|
||||
<?php if ($this->current_page > 1): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/my/<?php echo $this->current_page - 1; ?>" class="gallery-btn gallery-btn-secondary">Previous</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="gallery-pagination-info">Page <?php echo $this->current_page; ?> of <?php echo $total_pages; ?></span>
|
||||
|
||||
<?php if ($this->current_page < $total_pages): ?>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/my/<?php echo $this->current_page + 1; ?>" class="gallery-btn gallery-btn-secondary">Next</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
168
application/view/gallery/success.php
Normal file
168
application/view/gallery/success.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<div class="gallery-success-overlay">
|
||||
<div class="gallery-success-card">
|
||||
<div class="gallery-success-icon">
|
||||
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
||||
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
|
||||
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Upload Successful!</h2>
|
||||
<p>Your image has been uploaded successfully.</p>
|
||||
<div class="gallery-success-preview">
|
||||
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/thumb"
|
||||
alt="<?php echo htmlspecialchars($this->image->title); ?>">
|
||||
</div>
|
||||
<p class="gallery-success-title"><?php echo htmlspecialchars($this->image->title); ?></p>
|
||||
<p class="gallery-success-redirect">Redirecting to your image<span class="dots"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.gallery-success-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.gallery-success-card {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
animation: slideUp 0.4s ease 0.1s both;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-success-icon {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #4CAF50;
|
||||
stroke-miterlimit: 10;
|
||||
margin: 0 auto;
|
||||
box-shadow: inset 0px 0px 0px #4CAF50;
|
||||
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #4CAF50;
|
||||
fill: none;
|
||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0%, 100% { transform: none; }
|
||||
50% { transform: scale3d(1.1, 1.1, 1); }
|
||||
}
|
||||
|
||||
@keyframes fill {
|
||||
100% { box-shadow: inset 0px 0px 0px 40px rgba(76, 175, 80, 0.1); }
|
||||
}
|
||||
|
||||
.gallery-success-card h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gallery-success-card p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.gallery-success-preview {
|
||||
margin: 24px 0 16px;
|
||||
animation: previewFade 0.5s ease 0.6s both;
|
||||
}
|
||||
|
||||
@keyframes previewFade {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.gallery-success-preview img {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.gallery-success-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
|
||||
.gallery-success-redirect {
|
||||
font-size: 14px !important;
|
||||
color: #999 !important;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.dots::after {
|
||||
content: '';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0% { content: ''; }
|
||||
25% { content: '.'; }
|
||||
50% { content: '..'; }
|
||||
75% { content: '...'; }
|
||||
100% { content: ''; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
window.location.href = '<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>';
|
||||
}, 2500);
|
||||
</script>
|
||||
437
application/view/gallery/upload.php
Normal file
437
application/view/gallery/upload.php
Normal file
@@ -0,0 +1,437 @@
|
||||
<div class="gallery-container">
|
||||
<div class="gallery-header">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-back">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
Back to My Images
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-container">
|
||||
<h1>Upload Image</h1>
|
||||
|
||||
<?php $this->renderFeedbackMessages(); ?>
|
||||
|
||||
<form id="upload-form" method="post" action="<?php echo Config::get('URL'); ?>gallery/upload" enctype="multipart/form-data" class="gallery-form">
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-label">Image File</label>
|
||||
<div class="gallery-upload-zone" id="upload-zone">
|
||||
<input type="file" name="image" id="image-input" accept="image/jpeg,image/png,image/gif,image/webp" required>
|
||||
<div class="gallery-upload-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<span>Click or drag image here</span>
|
||||
<small>JPG, PNG, GIF, WebP - Max 10MB</small>
|
||||
</div>
|
||||
<div class="gallery-upload-preview" id="upload-preview" style="display: none;">
|
||||
<img id="preview-image" src="" alt="Preview">
|
||||
<button type="button" class="gallery-upload-remove" id="remove-preview">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-label" for="title">Title</label>
|
||||
<input type="text" name="title" id="title" class="gallery-form-input" placeholder="Give your image a title">
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-label" for="description">Description</label>
|
||||
<textarea name="description" id="description" class="gallery-form-textarea" rows="4" placeholder="Add a description (optional)"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-group">
|
||||
<label class="gallery-form-checkbox">
|
||||
<input type="checkbox" name="is_public" value="1" checked>
|
||||
<span>Make this image public</span>
|
||||
</label>
|
||||
<small class="gallery-form-hint">Public images are visible to everyone in the gallery</small>
|
||||
</div>
|
||||
|
||||
<div class="gallery-form-actions">
|
||||
<button type="submit" name="submit_upload" class="gallery-btn gallery-btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
Upload Image
|
||||
</button>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-btn gallery-btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress Overlay -->
|
||||
<div id="upload-overlay" class="upload-overlay" style="display: none;">
|
||||
<div class="upload-modal">
|
||||
<!-- Progress State -->
|
||||
<div id="upload-progress-state" class="upload-state">
|
||||
<div class="upload-icon uploading">
|
||||
<svg class="upload-spinner" viewBox="0 0 50 50">
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Uploading...</h2>
|
||||
<div class="upload-progress-bar">
|
||||
<div class="upload-progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
<p class="upload-progress-text"><span id="progress-percent">0</span>%</p>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div id="upload-success-state" class="upload-state" style="display: none;">
|
||||
<div class="upload-icon success">
|
||||
<svg class="checkmark" viewBox="0 0 52 52">
|
||||
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
|
||||
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Upload Complete!</h2>
|
||||
<p>Your image has been uploaded successfully.</p>
|
||||
<p class="upload-redirect">Redirecting<span class="dots"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="upload-error-state" class="upload-state" style="display: none;">
|
||||
<div class="upload-icon error">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Upload Failed</h2>
|
||||
<p id="error-message">Something went wrong. Please try again.</p>
|
||||
<button type="button" class="gallery-btn gallery-btn-primary" onclick="hideOverlay()">Try Again</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.upload-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.upload-modal {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.upload-state {
|
||||
animation: slideUp 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.upload-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-icon.uploading svg {
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
.upload-icon.success svg {
|
||||
stroke: #4CAF50;
|
||||
}
|
||||
|
||||
.upload-icon.error svg {
|
||||
stroke: #dc3545;
|
||||
}
|
||||
|
||||
.upload-spinner {
|
||||
animation: rotate 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.upload-spinner circle {
|
||||
stroke: #333;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: 0;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
|
||||
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
|
||||
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
|
||||
}
|
||||
|
||||
.upload-modal h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.upload-modal p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0 12px;
|
||||
}
|
||||
|
||||
.upload-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #333, #555);
|
||||
border-radius: 4px;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-progress-text {
|
||||
font-family: monospace;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* Checkmark Animation */
|
||||
.checkmark {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #4CAF50;
|
||||
stroke-miterlimit: 10;
|
||||
box-shadow: inset 0px 0px 0px #4CAF50;
|
||||
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #4CAF50;
|
||||
fill: none;
|
||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0%, 100% { transform: none; }
|
||||
50% { transform: scale3d(1.1, 1.1, 1); }
|
||||
}
|
||||
|
||||
@keyframes fill {
|
||||
100% { box-shadow: inset 0px 0px 0px 40px rgba(76, 175, 80, 0.1); }
|
||||
}
|
||||
|
||||
.upload-redirect {
|
||||
margin-top: 20px !important;
|
||||
font-size: 13px !important;
|
||||
color: #999 !important;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.dots::after {
|
||||
content: '';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0% { content: ''; }
|
||||
25% { content: '.'; }
|
||||
50% { content: '..'; }
|
||||
75% { content: '...'; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('upload-form');
|
||||
const uploadZone = document.getElementById('upload-zone');
|
||||
const imageInput = document.getElementById('image-input');
|
||||
const uploadPreview = document.getElementById('upload-preview');
|
||||
const previewImage = document.getElementById('preview-image');
|
||||
const removePreview = document.getElementById('remove-preview');
|
||||
const placeholder = uploadZone.querySelector('.gallery-upload-placeholder');
|
||||
|
||||
const overlay = document.getElementById('upload-overlay');
|
||||
const progressState = document.getElementById('upload-progress-state');
|
||||
const successState = document.getElementById('upload-success-state');
|
||||
const errorState = document.getElementById('upload-error-state');
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
const progressPercent = document.getElementById('progress-percent');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// Image preview
|
||||
imageInput.addEventListener('change', function(e) {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
previewImage.src = e.target.result;
|
||||
placeholder.style.display = 'none';
|
||||
uploadPreview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
removePreview.addEventListener('click', function() {
|
||||
imageInput.value = '';
|
||||
previewImage.src = '';
|
||||
placeholder.style.display = 'flex';
|
||||
uploadPreview.style.display = 'none';
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
uploadZone.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
this.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('dragover');
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
imageInput.files = e.dataTransfer.files;
|
||||
imageInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission with AJAX
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!imageInput.files || !imageInput.files[0]) {
|
||||
alert('Please select an image to upload');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(form);
|
||||
formData.append('submit_upload', '1');
|
||||
|
||||
// Show overlay with progress state
|
||||
showOverlay('progress');
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// Progress tracking
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
progressFill.style.width = percent + '%';
|
||||
progressPercent.textContent = percent;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function() {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.success) {
|
||||
showOverlay('success');
|
||||
// Redirect after animation
|
||||
setTimeout(function() {
|
||||
window.location.href = '<?php echo Config::get('URL'); ?>gallery/view/' + response.image_id;
|
||||
}, 2000);
|
||||
} else {
|
||||
showOverlay('error', response.message || 'Upload failed');
|
||||
}
|
||||
} catch (e) {
|
||||
showOverlay('error', 'Invalid server response');
|
||||
}
|
||||
} else {
|
||||
showOverlay('error', 'Server error: ' + xhr.status);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', function() {
|
||||
showOverlay('error', 'Network error. Please check your connection.');
|
||||
});
|
||||
|
||||
xhr.open('POST', form.action);
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
function showOverlay(state, message) {
|
||||
overlay.style.display = 'flex';
|
||||
progressState.style.display = 'none';
|
||||
successState.style.display = 'none';
|
||||
errorState.style.display = 'none';
|
||||
|
||||
if (state === 'progress') {
|
||||
progressState.style.display = 'block';
|
||||
progressFill.style.width = '0%';
|
||||
progressPercent.textContent = '0';
|
||||
} else if (state === 'success') {
|
||||
successState.style.display = 'block';
|
||||
} else if (state === 'error') {
|
||||
errorState.style.display = 'block';
|
||||
if (message) {
|
||||
errorMessage.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.hideOverlay = function() {
|
||||
overlay.style.display = 'none';
|
||||
};
|
||||
});
|
||||
</script>
|
||||
80
application/view/gallery/view.php
Normal file
80
application/view/gallery/view.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<div class="gallery-container">
|
||||
<div class="gallery-header">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/index" class="gallery-back">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
Back to Gallery
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gallery-view">
|
||||
<div class="gallery-view-image">
|
||||
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/full"
|
||||
alt="<?php echo htmlspecialchars($this->image->title); ?>">
|
||||
</div>
|
||||
|
||||
<div class="gallery-view-info">
|
||||
<h1><?php echo htmlspecialchars($this->image->title); ?></h1>
|
||||
|
||||
<div class="gallery-view-meta">
|
||||
<div class="gallery-view-author">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
<a href="<?php echo Config::get('URL'); ?>profile/showProfile/<?php echo $this->image->user_name; ?>">
|
||||
<?php echo htmlspecialchars($this->image->user_name); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gallery-view-date">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
<?php echo date('M j, Y', strtotime($this->image->created_at)); ?>
|
||||
</div>
|
||||
|
||||
<div class="gallery-view-size">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
<?php echo GalleryModel::formatFileSize($this->image->file_size); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($this->image->description): ?>
|
||||
<div class="gallery-view-description">
|
||||
<?php echo nl2br(htmlspecialchars($this->image->description)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($this->image->user_id == Session::get('user_id')): ?>
|
||||
<div class="gallery-view-actions">
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $this->image->id; ?>" class="gallery-btn gallery-btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
</svg>
|
||||
Edit
|
||||
</a>
|
||||
<a href="<?php echo Config::get('URL'); ?>gallery/delete/<?php echo $this->image->id; ?>"
|
||||
class="gallery-btn gallery-btn-danger"
|
||||
onclick="return confirm('Delete this image permanently?');">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,67 +1,66 @@
|
||||
<div class="dbmanager-container">
|
||||
<div class="dbmanager-sidebar">
|
||||
<h3>Databases</h3>
|
||||
<ul class="db-tree">
|
||||
<?php foreach ($this->databases as $db): ?>
|
||||
<li class="db-item <?php echo ($db === $this->database_name) ? 'active' : ''; ?>">
|
||||
<a href="<?php echo Config::get('URL'); ?>sql/index/<?php echo urlencode($db); ?>" class="db-link">
|
||||
<?php echo htmlspecialchars($db); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<div class="db-actions">
|
||||
<a href="<?php echo Config::get('URL'); ?>database/index" class="btn" style="display:block;text-align:center;">Back to Databases</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($this->history)): ?>
|
||||
<div style="margin-top:20px;">
|
||||
<h4>Query History</h4>
|
||||
<ul class="query-history">
|
||||
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
|
||||
<li class="history-item" onclick="document.getElementById('sql_query').value = this.dataset.query;" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
|
||||
<?php echo htmlspecialchars(substr($item['query_text'], 0, 40)); ?>...
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="btn btn-small btn-danger" style="margin-top:10px;">Clear History</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="dbm-content-header">
|
||||
<div class="dbm-breadcrumb">
|
||||
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
|
||||
<span class="separator">/</span>
|
||||
<span>SQL Console</span>
|
||||
</div>
|
||||
<div class="dbm-title">
|
||||
<h1>SQL Console</h1>
|
||||
<span class="badge"><?php echo htmlspecialchars($this->database_name); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dbmanager-content">
|
||||
<h2>SQL Console</h2>
|
||||
<p>Database: <strong><?php echo htmlspecialchars($this->database_name); ?></strong></p>
|
||||
|
||||
<div class="dbm-content-body">
|
||||
<div class="sql-console">
|
||||
<form method="post" action="<?php echo Config::get('URL'); ?>sql/execute" id="sql-form">
|
||||
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($this->database_name); ?>">
|
||||
|
||||
<div class="sql-editor">
|
||||
<textarea name="sql_query" id="sql_query" rows="6" placeholder="Enter your SQL query here...
|
||||
Example: SELECT * FROM users LIMIT 10"></textarea>
|
||||
<div class="sql-editor-container">
|
||||
<div class="sql-editor-wrapper">
|
||||
<pre class="sql-highlight" id="sql-highlight" aria-hidden="true"></pre>
|
||||
<textarea name="sql_query" id="sql_query" class="sql-textarea" spellcheck="false" placeholder="SELECT * FROM users LIMIT 10;"><?php echo isset($_POST['sql_query']) ? htmlspecialchars($_POST['sql_query']) : ''; ?></textarea>
|
||||
</div>
|
||||
<div class="sql-line-numbers" id="line-numbers">1</div>
|
||||
</div>
|
||||
|
||||
<div class="sql-actions">
|
||||
<button type="submit" class="btn">Execute Query</button>
|
||||
<button type="button" class="btn" style="background:#6c757d;" onclick="document.getElementById('sql_query').value = '';">Clear</button>
|
||||
<div class="sql-toolbar">
|
||||
<div class="sql-toolbar-left">
|
||||
<button type="submit" class="dbm-btn dbm-btn-success">
|
||||
<i data-lucide="play"></i>
|
||||
Execute
|
||||
</button>
|
||||
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="formatSQL()">
|
||||
<i data-lucide="align-left"></i>
|
||||
Format
|
||||
</button>
|
||||
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="clearSQL()">
|
||||
<i data-lucide="trash-2"></i>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="sql-toolbar-right">
|
||||
<span class="sql-hint">Ctrl+Enter to execute</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="sql-result" class="sql-result">
|
||||
<?php
|
||||
// Check for session result
|
||||
$result = Session::get('sql_result');
|
||||
if ($result) {
|
||||
Session::set('sql_result', null);
|
||||
|
||||
if ($result['success']) {
|
||||
echo '<div class="result-success">';
|
||||
echo '<p>' . htmlspecialchars($result['message']) . '</p>';
|
||||
echo '<p class="execution-time">Execution time: ' . $result['execution_time'] . ' ms</p>';
|
||||
echo '<div class="sql-result-success">';
|
||||
echo '<div class="sql-result-header">';
|
||||
echo '<span class="sql-result-status"><i data-lucide="check-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
|
||||
echo '<span class="sql-result-time">' . $result['execution_time'] . ' ms</span>';
|
||||
echo '</div>';
|
||||
|
||||
if (!empty($result['result'])) {
|
||||
echo '<div class="table-wrapper"><table class="data-table"><thead><tr>';
|
||||
echo '<div class="sql-result-table-wrapper"><table class="dbm-table"><thead><tr>';
|
||||
foreach (array_keys($result['result'][0]) as $col) {
|
||||
echo '<th>' . htmlspecialchars($col) . '</th>';
|
||||
}
|
||||
@@ -69,7 +68,7 @@ Example: SELECT * FROM users LIMIT 10"></textarea>
|
||||
foreach ($result['result'] as $row) {
|
||||
echo '<tr>';
|
||||
foreach ($row as $value) {
|
||||
echo '<td>' . ($value === null ? '<span style="color:#999;">NULL</span>' : htmlspecialchars(substr($value, 0, 100))) . '</td>';
|
||||
echo '<td>' . ($value === null ? '<span class="null-value">NULL</span>' : htmlspecialchars(substr($value, 0, 200))) . '</td>';
|
||||
}
|
||||
echo '</tr>';
|
||||
}
|
||||
@@ -77,206 +76,398 @@ Example: SELECT * FROM users LIMIT 10"></textarea>
|
||||
}
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="result-error">';
|
||||
echo '<p><strong>Error:</strong> ' . htmlspecialchars($result['message']) . '</p>';
|
||||
echo '<div class="sql-result-error">';
|
||||
echo '<div class="sql-result-header">';
|
||||
echo '<span class="sql-result-status"><i data-lucide="x-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
|
||||
echo '</div>';
|
||||
if (!empty($result['error'])) {
|
||||
echo '<p class="error-details">' . htmlspecialchars($result['error']) . '</p>';
|
||||
echo '<pre class="sql-error-details">' . htmlspecialchars($result['error']) . '</pre>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($this->history)): ?>
|
||||
<div class="sql-history">
|
||||
<h3>Recent Queries</h3>
|
||||
<div class="sql-history-list">
|
||||
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
|
||||
<div class="sql-history-item" onclick="loadQuery(this)" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
|
||||
<code><?php echo htmlspecialchars(substr($item['query_text'], 0, 80)); ?><?php echo strlen($item['query_text']) > 80 ? '...' : ''; ?></code>
|
||||
<span class="sql-history-time"><?php echo date('M j, H:i', strtotime($item['query_timestamp'])); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="dbm-btn dbm-btn-sm dbm-btn-secondary" style="margin-top: 12px;">Clear History</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dbmanager-container {
|
||||
.sql-console {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dbmanager-sidebar {
|
||||
width: 250px;
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
flex-shrink: 0;
|
||||
.sql-editor-container {
|
||||
display: flex;
|
||||
border: 1px solid var(--dbm-border);
|
||||
border-radius: var(--dbm-radius);
|
||||
overflow: hidden;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.dbmanager-sidebar h3, .dbmanager-sidebar h4 {
|
||||
margin: 0 0 10px 0;
|
||||
.sql-line-numbers {
|
||||
padding: 12px 8px;
|
||||
background: #252526;
|
||||
color: #858585;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
min-width: 40px;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.db-tree {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.db-item {
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.db-item:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.db-item.active {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.db-item.active .db-link {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.db-link {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.query-history {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 5px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #ddd;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.db-actions {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.dbmanager-content {
|
||||
.sql-editor-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.dbmanager-content h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sql-editor textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
.sql-highlight, .sql-textarea {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.sql-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
color: #d4d4d4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sql-textarea {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
caret-color: #fff;
|
||||
resize: none;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sql-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sql-result {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.execution-time {
|
||||
font-size: 12px;
|
||||
.sql-textarea::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
/* Syntax highlighting colors - VS Code dark theme */
|
||||
.sql-highlight .sql-keyword { color: #569cd6 !important; font-weight: bold !important; }
|
||||
.sql-highlight .sql-function { color: #dcdcaa !important; }
|
||||
.sql-highlight .sql-string { color: #ce9178 !important; }
|
||||
.sql-highlight .sql-number { color: #b5cea8 !important; }
|
||||
.sql-highlight .sql-operator { color: #d4d4d4 !important; }
|
||||
.sql-highlight .sql-comment { color: #6a9955 !important; font-style: italic !important; }
|
||||
.sql-highlight .sql-table { color: #4ec9b0 !important; }
|
||||
.sql-highlight .sql-column { color: #9cdcfe !important; }
|
||||
|
||||
.sql-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sql-toolbar-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sql-hint {
|
||||
font-size: 12px;
|
||||
color: var(--dbm-text-muted);
|
||||
}
|
||||
|
||||
.sql-result {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sql-result-success {
|
||||
background: #f0f9f0;
|
||||
border: 1px solid #c3e6c3;
|
||||
border-radius: var(--dbm-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sql-result-error {
|
||||
background: #fdf0f0;
|
||||
border: 1px solid #f5c6c6;
|
||||
border-radius: var(--dbm-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sql-result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: rgba(0,0,0,0.03);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.sql-result-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sql-result-success .sql-result-status { color: #2e7d32; }
|
||||
.sql-result-error .sql-result-status { color: #c62828; }
|
||||
|
||||
.sql-result-time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.sql-result-table-wrapper {
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sql-result-table-wrapper .dbm-table {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sql-result-table-wrapper .dbm-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f8f9fa;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.null-value {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sql-error-details {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #721c24;
|
||||
color: #c62828;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-top: 15px;
|
||||
.sql-history {
|
||||
background: var(--dbm-bg-secondary);
|
||||
border-radius: var(--dbm-radius);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
.sql-history h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--dbm-text);
|
||||
}
|
||||
|
||||
.sql-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sql-history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--dbm-bg);
|
||||
border-radius: var(--dbm-radius);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sql-history-item:hover {
|
||||
background: var(--dbm-bg-tertiary);
|
||||
}
|
||||
|
||||
.sql-history-item code {
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 200px;
|
||||
color: var(--dbm-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.sql-history-time {
|
||||
font-size: 11px;
|
||||
color: var(--dbm-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const textarea = document.getElementById('sql_query');
|
||||
const highlight = document.getElementById('sql-highlight');
|
||||
const lineNumbers = document.getElementById('line-numbers');
|
||||
|
||||
function updateHighlight() {
|
||||
const code = textarea.value;
|
||||
highlight.innerHTML = highlightSQL(code) + '\n';
|
||||
updateLineNumbers();
|
||||
}
|
||||
|
||||
function updateLineNumbers() {
|
||||
const lines = textarea.value.split('\n').length;
|
||||
let nums = '';
|
||||
for (let i = 1; i <= lines; i++) {
|
||||
nums += i + '\n';
|
||||
}
|
||||
lineNumbers.textContent = nums;
|
||||
}
|
||||
|
||||
function highlightSQL(code) {
|
||||
if (!code) return '';
|
||||
|
||||
// Escape HTML
|
||||
code = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Use inline styles for guaranteed coloring
|
||||
const styles = {
|
||||
keyword: 'color:#569cd6;font-weight:bold',
|
||||
function: 'color:#dcdcaa',
|
||||
string: 'color:#ce9178',
|
||||
number: 'color:#b5cea8',
|
||||
comment: 'color:#6a9955;font-style:italic'
|
||||
};
|
||||
|
||||
// Comments (must be first to avoid highlighting keywords inside comments)
|
||||
code = code.replace(/(--[^\n]*)/g, '<span style="' + styles.comment + '">$1</span>');
|
||||
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span style="' + styles.comment + '">$1</span>');
|
||||
|
||||
// Strings
|
||||
code = code.replace(/('[^']*')/g, '<span style="' + styles.string + '">$1</span>');
|
||||
code = code.replace(/("[^"]*")/g, '<span style="' + styles.string + '">$1</span>');
|
||||
|
||||
// Numbers (but not inside already-styled spans)
|
||||
code = code.replace(/\b(\d+\.?\d*)\b(?![^<]*>)/g, '<span style="' + styles.number + '">$1</span>');
|
||||
|
||||
// Keywords
|
||||
const keywords = [
|
||||
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'NULL',
|
||||
'ORDER', 'BY', 'ASC', 'DESC', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
|
||||
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
|
||||
'CREATE', 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'TRIGGER', 'PROCEDURE', 'FUNCTION',
|
||||
'ALTER', 'DROP', 'TRUNCATE', 'ADD', 'MODIFY', 'CHANGE', 'RENAME',
|
||||
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'OUTER', 'CROSS', 'ON', 'USING',
|
||||
'UNION', 'ALL', 'DISTINCT', 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
||||
'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'DEFAULT',
|
||||
'AUTO_INCREMENT', 'ENGINE', 'CHARSET', 'COLLATE',
|
||||
'IF', 'EXISTS', 'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE',
|
||||
'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION'
|
||||
];
|
||||
|
||||
const keywordRegex = new RegExp('\\b(' + keywords.join('|') + ')\\b(?![^<]*>)', 'gi');
|
||||
code = code.replace(keywordRegex, '<span style="' + styles.keyword + '">$1</span>');
|
||||
|
||||
// Functions
|
||||
const functions = [
|
||||
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH', 'UPPER', 'LOWER',
|
||||
'TRIM', 'LTRIM', 'RTRIM', 'REPLACE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST', 'CONVERT',
|
||||
'DATE', 'NOW', 'CURDATE', 'CURTIME', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND',
|
||||
'DATE_FORMAT', 'DATEDIFF', 'DATE_ADD', 'DATE_SUB', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD',
|
||||
'RAND', 'UUID', 'MD5', 'SHA1', 'SHA2', 'GROUP_CONCAT', 'JSON_OBJECT', 'JSON_ARRAY'
|
||||
];
|
||||
|
||||
const funcRegex = new RegExp('\\b(' + functions.join('|') + ')\\s*\\(', 'gi');
|
||||
code = code.replace(funcRegex, '<span style="' + styles.function + '">$1</span>(');
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
textarea.addEventListener('input', updateHighlight);
|
||||
textarea.addEventListener('scroll', function() {
|
||||
highlight.scrollTop = textarea.scrollTop;
|
||||
highlight.scrollLeft = textarea.scrollLeft;
|
||||
});
|
||||
|
||||
// Ctrl+Enter to execute
|
||||
textarea.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
document.getElementById('sql-form').submit();
|
||||
}
|
||||
// Tab to insert spaces
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 4;
|
||||
updateHighlight();
|
||||
}
|
||||
});
|
||||
|
||||
updateHighlight();
|
||||
lucide.createIcons();
|
||||
});
|
||||
|
||||
function loadQuery(element) {
|
||||
document.getElementById('sql_query').value = element.dataset.query;
|
||||
document.getElementById('sql_query').dispatchEvent(new Event('input'));
|
||||
document.getElementById('sql_query').focus();
|
||||
}
|
||||
|
||||
function clearSQL() {
|
||||
document.getElementById('sql_query').value = '';
|
||||
document.getElementById('sql_query').dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
function formatSQL() {
|
||||
const textarea = document.getElementById('sql_query');
|
||||
let sql = textarea.value;
|
||||
|
||||
// Basic formatting
|
||||
const keywords = ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'ON', 'SET', 'VALUES', 'INSERT INTO', 'UPDATE', 'DELETE FROM'];
|
||||
|
||||
keywords.forEach(kw => {
|
||||
const regex = new RegExp('\\b' + kw.replace(' ', '\\s+') + '\\b', 'gi');
|
||||
sql = sql.replace(regex, '\n' + kw);
|
||||
});
|
||||
|
||||
sql = sql.trim();
|
||||
textarea.value = sql;
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user