mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-04 04:22:58 +00:00
3208 lines
116 KiB
HTML
3208 lines
116 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="cs">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Dashboard - PP Kunovice</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
:root {
|
|
--primary-color: #4a6cf7;
|
|
--primary-hover: #3a56d4;
|
|
--secondary-color: #6c757d;
|
|
--success-color: #28a745;
|
|
--danger-color: #dc3545;
|
|
--warning-color: #ffc107;
|
|
--info-color: #17a2b8;
|
|
--light-color: #f8f9fa;
|
|
--dark-color: #212529;
|
|
--border-color: #e9ecef;
|
|
--border-radius: 8px;
|
|
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
--transition: all 0.3s ease;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #f8f9fa;
|
|
color: #333;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.header {
|
|
background-color: #fff;
|
|
color: var(--dark-color);
|
|
padding: 1rem 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
box-shadow: var(--box-shadow);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.logout-btn {
|
|
background-color: transparent;
|
|
border: 1px solid var(--primary-color);
|
|
color: var(--primary-color);
|
|
padding: 0.5rem 1.2rem;
|
|
border-radius: var(--border-radius);
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.logout-btn:hover {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 2rem auto;
|
|
padding: 0 1.5rem;
|
|
}
|
|
|
|
.dashboard-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.card {
|
|
background-color: white;
|
|
border-radius: var(--border-radius);
|
|
padding: 1.75rem;
|
|
box-shadow: var(--box-shadow);
|
|
transition: var(--transition);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.card h3 {
|
|
margin-top: 0;
|
|
color: var(--dark-color);
|
|
font-weight: 600;
|
|
font-size: 1.25rem;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.75rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 500;
|
|
color: var(--dark-color);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.form-group input[type="text"],
|
|
.form-group input[type="url"],
|
|
.form-group input[type="number"],
|
|
.form-group textarea,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
font-size: 1rem;
|
|
transition: var(--transition);
|
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.form-group input[type="text"]:focus,
|
|
.form-group input[type="url"]:focus,
|
|
.form-group input[type="number"]:focus,
|
|
.form-group textarea:focus,
|
|
.form-group select:focus {
|
|
border-color: var(--primary-color);
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15);
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 120px;
|
|
resize: vertical;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.color-preview {
|
|
display: inline-block;
|
|
width: 30px;
|
|
height: 30px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 50%;
|
|
vertical-align: middle;
|
|
margin-left: 10px;
|
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.color-picker {
|
|
margin-left: 10px;
|
|
vertical-align: middle;
|
|
height: 30px;
|
|
padding: 0;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.style-presets {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.style-preset {
|
|
padding: 8px 16px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
transition: var(--transition);
|
|
background-color: white;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.style-preset:hover {
|
|
background-color: var(--light-color);
|
|
border-color: var(--secondary-color);
|
|
transform: translateY(-2px);
|
|
}
|
|
.banner-preview {
|
|
margin: 2rem 0;
|
|
padding: 0;
|
|
border: 2px dashed var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
background-color: #fff;
|
|
display: none;
|
|
position: relative;
|
|
overflow: hidden;
|
|
min-height: 180px;
|
|
transition: var(--transition);
|
|
box-shadow: var(--box-shadow);
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.banner-preview:hover {
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.banner-preview::before {
|
|
content: 'Náhled banneru';
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background-color: rgba(255,255,255,0.8);
|
|
padding: 4px 10px;
|
|
border-radius: 4px;
|
|
font-size: 0.8rem;
|
|
color: var(--secondary-color);
|
|
z-index: 3;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.banner-preview img {
|
|
max-width: 100%;
|
|
max-height: 300px;
|
|
object-fit: contain;
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.banner-preview.with-image {
|
|
min-height: 220px;
|
|
}
|
|
|
|
.banner-preview-content {
|
|
position: relative;
|
|
z-index: 2;
|
|
text-align: center;
|
|
padding: 20px;
|
|
margin: 0;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
display: block;
|
|
}
|
|
|
|
.banner-preview-bg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
opacity: 0.7;
|
|
z-index: 1;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.banner-preview-text {
|
|
position: relative;
|
|
z-index: 2;
|
|
margin: 0;
|
|
padding: 0;
|
|
line-height: 1.5;
|
|
word-wrap: break-word;
|
|
}
|
|
.color-picker-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.color-picker {
|
|
vertical-align: middle;
|
|
margin-right: 8px;
|
|
cursor: pointer;
|
|
width: 40px;
|
|
height: 40px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
background: none;
|
|
}
|
|
.image-upload-container {
|
|
margin: 15px 0;
|
|
padding: 15px;
|
|
border: 1px dashed #ddd;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
}
|
|
.image-preview {
|
|
max-width: 200px;
|
|
max-height: 150px;
|
|
margin: 10px auto;
|
|
display: block;
|
|
}
|
|
.card p {
|
|
color: #666;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Form Actions */
|
|
.form-actions {
|
|
margin-top: 2.5rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--border-color);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0.75rem 1.5rem;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--border-radius);
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn i {
|
|
margin-right: 0.5rem;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: var(--primary-hover);
|
|
border-color: var(--primary-hover);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: var(--secondary-color);
|
|
color: white;
|
|
border-color: var(--secondary-color);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #5a6268;
|
|
border-color: #545b62;
|
|
}
|
|
|
|
.btn-danger {
|
|
background-color: var(--danger-color);
|
|
color: white;
|
|
border-color: var(--danger-color);
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: #c82333;
|
|
border-color: #bd2130;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.65;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
/* Notifications */
|
|
.notification {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 16px 24px;
|
|
border-radius: var(--border-radius);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
z-index: 1000;
|
|
animation: fadeIn 0.3s, fadeOut 0.3s 2.7s forwards;
|
|
min-width: 300px;
|
|
max-width: 450px;
|
|
}
|
|
|
|
.notification i {
|
|
margin-right: 12px;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.notification.info {
|
|
background-color: var(--info-color);
|
|
border-left: 4px solid #117a8b;
|
|
}
|
|
|
|
.notification.success {
|
|
background-color: var(--success-color);
|
|
border-left: 4px solid #1e7e34;
|
|
}
|
|
|
|
.notification.warning {
|
|
background-color: var(--warning-color);
|
|
color: #212529;
|
|
border-left: 4px solid #d39e00;
|
|
}
|
|
|
|
.notification.error {
|
|
background-color: var(--danger-color);
|
|
border-left: 4px solid #bd2130;
|
|
}
|
|
|
|
.notification.fade-out {
|
|
animation: fadeOut 0.3s forwards;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeOut {
|
|
to {
|
|
opacity: 0;
|
|
transform: translateY(-20px);
|
|
}
|
|
}
|
|
|
|
/* Additional responsive styles */
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
.card {
|
|
padding: 1.25rem;
|
|
}
|
|
|
|
.notification {
|
|
left: 20px;
|
|
right: 20px;
|
|
max-width: calc(100% - 40px);
|
|
}
|
|
|
|
.form-actions {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-actions .btn {
|
|
width: 100%;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
}
|
|
|
|
.drag-drop-area {
|
|
border: 2px dashed var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 30px 20px;
|
|
text-align: center;
|
|
background-color: #f8f9fa;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
margin-bottom: 15px;
|
|
position: relative;
|
|
}
|
|
|
|
.drag-drop-area:hover, .drag-drop-area.dragover {
|
|
border-color: var(--primary-color);
|
|
background-color: rgba(74, 108, 247, 0.05);
|
|
}
|
|
|
|
.drag-drop-message {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.drag-drop-message i {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 15px;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.drag-drop-message p {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.image-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.image-preview {
|
|
max-width: 100%;
|
|
max-height: 200px;
|
|
border-radius: 4px;
|
|
margin-top: 10px;
|
|
display: none;
|
|
}
|
|
|
|
.banner-preview.with-image {
|
|
min-height: 220px;
|
|
}
|
|
|
|
.draggable-image {
|
|
cursor: move;
|
|
position: relative;
|
|
z-index: 10;
|
|
transition: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.draggable-image.dragging {
|
|
opacity: 0.8;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.banner-preview-container {
|
|
margin: 20px 0;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.banner-preview {
|
|
width: 100%;
|
|
min-height: 200px;
|
|
background: #ffffff;
|
|
border: 1px dashed #adb5bd;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.banner-preview img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.banner-preview .banner-text {
|
|
padding: 20px;
|
|
text-align: center;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Upload Box Styles */
|
|
.upload-box {
|
|
border: 2px dashed #dee2e6;
|
|
border-radius: 8px;
|
|
background-color: #f8f9fa;
|
|
transition: all 0.3s ease;
|
|
overflow: hidden;
|
|
min-height: 200px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.upload-box.dragover {
|
|
border-color: #0d6efd;
|
|
background-color: rgba(13, 110, 253, 0.05);
|
|
}
|
|
|
|
.upload-prompt {
|
|
padding: 2rem;
|
|
max-width: 320px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.upload-icon {
|
|
color: #adb5bd;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.upload-box:hover .upload-icon {
|
|
color: #6c757d;
|
|
}
|
|
|
|
/* Image Preview Styles */
|
|
.image-preview {
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.preview-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
background-color: #f1f3f5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 180px;
|
|
}
|
|
|
|
.preview-container img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.preview-overlay {
|
|
position: absolute;
|
|
bottom: 1rem;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.preview-container:hover .preview-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
.preview-overlay .btn {
|
|
width: 36px;
|
|
height: 36px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
/* Position Controls */
|
|
.position-controls .btn-group {
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.position-controls .btn {
|
|
transition: all 0.2s;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.position-controls .btn.active {
|
|
background-color: #0d6efd;
|
|
color: white;
|
|
border-color: #0d6efd;
|
|
}
|
|
|
|
/* Responsive Adjustments */
|
|
@media (max-width: 768px) {
|
|
.upload-box {
|
|
min-height: 160px;
|
|
}
|
|
|
|
.upload-prompt {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.preview-overlay {
|
|
opacity: 1;
|
|
bottom: 0.5rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Admin Dashboard</h1>
|
|
<button class="logout-btn" id="logoutBtn">Odhlásit se</button>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<h2>Vítejte v administraci</h2>
|
|
|
|
<!-- Apps Management Section -->
|
|
<div class="card" style="margin: 2rem auto; max-width: 1000px;">
|
|
<h3>Správa aplikací</h3>
|
|
|
|
<div class="mb-6">
|
|
<button id="addAppBtn" class="btn btn-primary">
|
|
<i class="fas fa-plus mr-2"></i>Přidat aplikaci
|
|
</button>
|
|
</div>
|
|
|
|
<div id="appsList" class="space-y-4">
|
|
<div class="mb-6">
|
|
<h4 class="font-medium text-gray-700 mb-3">Přednastavené aplikace</h4>
|
|
<div id="hardcodedAppsList" class="space-y-4">
|
|
<!-- Hardcoded apps will be loaded here -->
|
|
<div class="text-center text-gray-500 py-4">Načítám přednastavené aplikace...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 mb-4 border-t border-gray-200 pt-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h4 class="font-medium text-gray-700">Vlastní aplikace</h4>
|
|
<button id="addAppBtn" class="btn btn-primary">
|
|
<i class="fas fa-plus mr-2"></i>Přidat aplikaci
|
|
</button>
|
|
</div>
|
|
<div id="dynamicAppsList" class="space-y-4">
|
|
<!-- Dynamic apps will be loaded here -->
|
|
<div class="text-center text-gray-500 py-4">Načítám vlastní aplikace...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit App Modal -->
|
|
<div id="appModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
|
|
<div class="bg-white rounded-lg p-6 w-full max-w-md">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-xl font-semibold" id="appModalTitle">Přidat aplikaci</h3>
|
|
<button id="closeAppModal" class="text-gray-500 hover:text-gray-700">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="appForm" class="space-y-4">
|
|
<input type="hidden" id="appId">
|
|
|
|
<div class="form-group">
|
|
<label for="appName">Název aplikace</label>
|
|
<input type="text" id="appName" class="form-control" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="appUrl">URL adresa</label>
|
|
<input type="url" id="appUrl" class="form-control" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="appDescription">Popis (nepovinné)</label>
|
|
<textarea id="appDescription" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="appIcon">Ikona (nepovinné)</label>
|
|
<div class="mt-1 flex items-center">
|
|
<div class="relative">
|
|
<input type="file" id="appIcon" class="hidden" accept="image/*">
|
|
<label for="appIcon" class="cursor-pointer bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
<i class="fas fa-upload mr-2"></i>Vybrat soubor
|
|
</label>
|
|
<div id="fileName" class="text-sm text-gray-500 ml-2 truncate max-w-xs">Nebyl vybrán žádný soubor</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 flex items-center">
|
|
<img id="appIconPreview" src="" alt="Náhled ikony" class="h-12 w-12 rounded-full object-cover hidden">
|
|
<span class="text-xs text-gray-500 ml-2">Doporučená velikost: 64x64px (PNG, JPG, SVG)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button type="button" id="cancelAppBtn" class="btn btn-secondary">Zrušit</button>
|
|
<button type="submit" class="btn btn-primary">Uložit</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin: 2rem auto; max-width: 1000px;">
|
|
<h3>Správa banneru</h3>
|
|
|
|
<div class="form-group">
|
|
<div class="form-check form-switch mb-3">
|
|
<input class="form-check-input" type="checkbox" id="bannerVisibility" checked>
|
|
<label class="form-check-label" for="bannerVisibility">Viditelnost banneru</label>
|
|
</div>
|
|
<input type="hidden" id="bannerVisible" name="isVisible" value="true">
|
|
|
|
<div class="mb-3">
|
|
<label for="bannerText" class="form-label">Text banneru (HTML povoleno):</label>
|
|
<div class="border rounded">
|
|
<div id="bannerTextToolbar" class="bg-light p-2 border-bottom d-flex gap-1 flex-wrap">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="bold" title="Tučné">
|
|
<i class="fas fa-bold"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="italic" title="Kurzíva">
|
|
<i class="fas fa-italic"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="underline" title="Podtržení">
|
|
<i class="fas fa-underline"></i>
|
|
</button>
|
|
<div class="vr"></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="insertUnorderedList" title="Odrážkový seznam">
|
|
<i class="fas fa-list-ul"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="insertOrderedList" title="Číslovaný seznam">
|
|
<i class="fas fa-list-ol"></i>
|
|
</button>
|
|
<div class="vr"></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="createLink" title="Vložit odkaz">
|
|
<i class="fas fa-link"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-command="unlink" title="Odebrat odkaz">
|
|
<i class="fas fa-unlink"></i>
|
|
</button>
|
|
</div>
|
|
<div id="bannerText" class="form-control" contenteditable="true" style="min-height: 100px; padding: 10px; overflow-y: auto;"></div>
|
|
<input type="hidden" id="bannerTextHidden" name="bannerText">
|
|
</div>
|
|
<div class="form-text">Můžete použít HTML značky pro formátování textu</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<label for="bannerLink">Odkaz (volitelný):</label>
|
|
<input type="url" id="bannerLink" class="form-control" placeholder="https://example.com">
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<label class="form-label">Pozice obrázku:</label>
|
|
<div class="btn-group w-100" role="group">
|
|
<button type="button" class="btn btn-outline-primary position-btn active" data-position="left">
|
|
<i class="fas fa-align-left me-1"></i> Vlevo
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary position-btn" data-position="right">
|
|
<i class="fas fa-align-right me-1"></i> Vpravo
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<form id="bannerForm">
|
|
<!-- Templates Section -->
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-medium text-gray-900 mb-3">Vyberte šablonu</h3>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-6" id="templateGrid">
|
|
<!-- Templates will be inserted here by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label fw-bold mb-2">Obrázek banneru</label>
|
|
|
|
<!-- Upload Box -->
|
|
<div id="dropArea" class="upload-box">
|
|
<input type="file" id="fileInput" accept="image/*" style="display: none;">
|
|
|
|
<!-- Upload Prompt -->
|
|
<div id="uploadPrompt" class="upload-prompt text-center p-4">
|
|
<div class="upload-icon mb-3">
|
|
<i class="fas fa-image fa-3x text-muted"></i>
|
|
</div>
|
|
<h5 class="mb-2">Přetáhněte obrázek sem</h5>
|
|
<p class="text-muted small mb-3">Nebo</p>
|
|
<button type="button" class="btn btn-primary btn-sm" id="uploadBtn">
|
|
<i class="fas fa-upload me-2"></i>Vybrat soubor
|
|
</button>
|
|
<p class="small text-muted mt-2 mb-0">Podporované formáty: JPG, PNG, GIF (max. 5MB)</p>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="form-section">
|
|
<h3>Náhled banneru</h3>
|
|
<div id="bannerPreviewContainer" class="banner-preview-container">
|
|
<div id="bannerPreview" class="banner-preview">
|
|
<div id="bannerPreviewContent" style="width: 100%; height: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" id="saveBannerBtn" class="btn btn-primary">
|
|
<i class="fas fa-save"></i> Uložit banner
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Get token and check authentication
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
window.location.href = '/login.html';
|
|
}
|
|
|
|
// Rich text editor functionality
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const bannerText = document.getElementById('bannerText');
|
|
const bannerTextHidden = document.getElementById('bannerTextHidden');
|
|
const toolbar = document.getElementById('bannerTextToolbar');
|
|
|
|
// Initialize hidden input with empty content
|
|
bannerTextHidden.value = bannerText.innerHTML;
|
|
|
|
// Update hidden input when content changes
|
|
bannerText.addEventListener('input', function() {
|
|
bannerTextHidden.value = this.innerHTML;
|
|
});
|
|
|
|
// Handle toolbar button clicks
|
|
toolbar.addEventListener('click', function(e) {
|
|
const button = e.target.closest('button[data-command]');
|
|
if (!button) return;
|
|
|
|
const command = button.dataset.command;
|
|
document.execCommand(command, false, command === 'createLink' ? prompt('Zadejte URL:') : null);
|
|
bannerText.focus();
|
|
});
|
|
|
|
// Handle paste to clean up pasted content
|
|
bannerText.addEventListener('paste', function(e) {
|
|
e.preventDefault();
|
|
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
|
|
document.execCommand('insertHTML', false, text);
|
|
});
|
|
|
|
// Handle tab key for indentation
|
|
bannerText.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
document.execCommand('insertHTML', false, ' ');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Show notification to user
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification ${type}`;
|
|
|
|
// Set icon based on notification type
|
|
let icon = 'info-circle';
|
|
if (type === 'success') icon = 'check-circle';
|
|
else if (type === 'error') icon = 'exclamation-circle';
|
|
else if (type === 'warning') icon = 'exclamation-triangle';
|
|
|
|
notification.innerHTML = `
|
|
<i class="fas fa-${icon}"></i>
|
|
<span>${message}</span>
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Auto-remove notification after delay
|
|
const delay = type === 'error' ? 5000 : 3000;
|
|
setTimeout(() => {
|
|
notification.classList.add('fade-out');
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, delay);
|
|
}
|
|
|
|
// Override fetch to include token (but NOT for FormData requests)
|
|
const originalFetch = window.fetch;
|
|
window.fetch = async function(resource, init = {}) {
|
|
// Add token to headers if it's an API request
|
|
if (typeof resource === 'string' && resource.startsWith('/api/')) {
|
|
const headers = new Headers(init.headers || {});
|
|
if (token) {
|
|
headers.set('Authorization', `Bearer ${token}`);
|
|
}
|
|
|
|
// Only set content type if not FormData (FormData sets its own)
|
|
if (!headers.has('Content-Type') && init.body && !(init.body instanceof FormData)) {
|
|
headers.set('Content-Type', 'application/json');
|
|
}
|
|
|
|
init.headers = headers;
|
|
}
|
|
|
|
return originalFetch(resource, init);
|
|
};
|
|
|
|
// Image handling - Drag and Drop functionality
|
|
let dragDropArea, uploadImageBtn, bannerImageElement;
|
|
|
|
// Prevent default behavior for drag events
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Visual feedback when dragging over the area
|
|
function highlight() {
|
|
if (dragDropArea) dragDropArea.classList.add('dragover');
|
|
}
|
|
|
|
function unhighlight() {
|
|
if (dragDropArea) dragDropArea.classList.remove('dragover');
|
|
}
|
|
|
|
// Initialize drag and drop functionality
|
|
function initDragAndDrop() {
|
|
dragDropArea = document.getElementById('dragDropArea');
|
|
uploadImageBtn = document.getElementById('uploadImageBtn');
|
|
bannerImage = document.getElementById('bannerImage');
|
|
|
|
if (!dragDropArea || !uploadImageBtn || !bannerImage) return;
|
|
|
|
// Click on drag area to select file
|
|
dragDropArea.addEventListener('click', function() {
|
|
bannerImage.click();
|
|
});
|
|
|
|
// Click on upload button to select file
|
|
uploadImageBtn.addEventListener('click', function() {
|
|
bannerImage.click();
|
|
});
|
|
|
|
// Prevent default behavior for drag events
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
dragDropArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
// Visual feedback when dragging over the area
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dragDropArea.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dragDropArea.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
// Handle dropped files
|
|
dragDropArea.addEventListener('drop', handleDrop, false);
|
|
}
|
|
|
|
// Update banner preview
|
|
function updateBannerPreview() {
|
|
const bannerPreview = document.getElementById('bannerPreview');
|
|
if (!bannerPreview) return;
|
|
|
|
// Get current values
|
|
const text = document.getElementById('bannerText').value || 'Náhled textu banneru';
|
|
const bgColor = document.getElementById('bannerBgColor')?.value || '#f8f9fa';
|
|
const textColor = document.getElementById('bannerTextColor')?.value || '#212529';
|
|
const textAlign = document.getElementById('bannerTextAlign')?.value || 'center';
|
|
const fontSize = document.getElementById('bannerFontSize')?.value || '1rem';
|
|
const padding = document.getElementById('bannerPadding')?.value || '20px';
|
|
const margin = document.getElementById('bannerMargin')?.value || '10px 0';
|
|
const borderRadius = document.getElementById('bannerBorderRadius')?.value || '4px';
|
|
const isVisible = document.getElementById('bannerVisibility')?.checked !== false;
|
|
|
|
// Build preview HTML
|
|
let previewHTML = `
|
|
<div class="banner-preview-content" style="
|
|
background: ${bgColor};
|
|
color: ${textColor};
|
|
text-align: ${textAlign};
|
|
font-size: ${fontSize};
|
|
padding: ${padding};
|
|
margin: ${margin};
|
|
border-radius: ${borderRadius};
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: ${textAlign};
|
|
position: relative;
|
|
overflow: hidden;
|
|
">
|
|
<div class="banner-text">${text}</div>
|
|
`;
|
|
|
|
previewHTML += '</div>';
|
|
|
|
// Update preview
|
|
bannerPreview.innerHTML = previewHTML;
|
|
|
|
// No custom positioning, always right-aligned
|
|
}
|
|
|
|
// Initialize template object
|
|
let template = {
|
|
containerStyle: '',
|
|
textStyle: '',
|
|
bgColor: '#f8f9fa',
|
|
textColor: '#212529',
|
|
textAlign: 'left',
|
|
fontSize: 16,
|
|
padding: 20,
|
|
margin: 20,
|
|
borderRadius: 8
|
|
};
|
|
|
|
// Initialize when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initialize banner preview elements
|
|
bannerVisible = document.getElementById('bannerVisibility');
|
|
bannerBgColor = document.getElementById('bannerBgColor');
|
|
bannerTextColor = document.getElementById('bannerTextColor');
|
|
bannerText = document.getElementById('bannerText');
|
|
bannerTextAlign = document.getElementById('bannerTextAlign');
|
|
bannerFontSize = document.getElementById('bannerFontSize');
|
|
bannerPadding = document.getElementById('bannerPadding');
|
|
bannerMargin = document.getElementById('bannerMargin');
|
|
bannerBorderRadius = document.getElementById('bannerBorderRadius');
|
|
bannerPreview = document.getElementById('bannerPreview');
|
|
|
|
// Initialize drag and drop and image upload
|
|
initDragAndDrop();
|
|
|
|
// Set up file input change event
|
|
const bannerImageInput = document.getElementById('bannerImage');
|
|
if (bannerImageInput) {
|
|
bannerImageInput.addEventListener('change', handleImageUpload);
|
|
}
|
|
|
|
// Set up event listeners for preview updates
|
|
const previewInputs = [
|
|
bannerVisible, bannerBgColor, bannerTextColor, bannerText,
|
|
bannerTextAlign, bannerFontSize, bannerPadding, bannerMargin, bannerBorderRadius
|
|
];
|
|
|
|
previewInputs.forEach(input => {
|
|
if (input) {
|
|
input.addEventListener('input', updateBannerPreview);
|
|
input.addEventListener('change', updateBannerPreview);
|
|
}
|
|
});
|
|
|
|
// Initialize upload functionality
|
|
const uploadPrompt = document.getElementById('uploadPrompt');
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
|
const dropArea = document.getElementById('dropArea');
|
|
const fileInput = document.getElementById('fileInput');
|
|
|
|
// Image is always on the right side - no position selector needed
|
|
|
|
// Handle file selection
|
|
function handleFileSelect(file) {
|
|
if (!file) return;
|
|
|
|
// Check file type
|
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
if (!validTypes.includes(file.type)) {
|
|
showNotification('Nepodporovaný formát souboru. Povolené formáty: JPG, PNG, GIF', 'error');
|
|
return;
|
|
}
|
|
|
|
// Check file size (5MB max)
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
showNotification('Soubor je příliš velký. Maximální velikost je 5MB.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Process the file
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
try {
|
|
currentImage = e.target.result;
|
|
updateBannerPreview();
|
|
} catch (error) {
|
|
console.error('Error processing file:', error);
|
|
showNotification('Chyba při zpracování souboru', 'error');
|
|
}
|
|
};
|
|
|
|
reader.onerror = function() {
|
|
console.error('Error reading file');
|
|
showNotification('Chyba při čtení souboru', 'error');
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
// Handle file input change
|
|
fileInput.addEventListener('change', function() {
|
|
if (this.files && this.files[0]) {
|
|
handleFileSelect(this.files[0]);
|
|
}
|
|
});
|
|
|
|
// Handle upload button click
|
|
if (uploadBtn) {
|
|
uploadBtn.addEventListener('click', () => fileInput.click());
|
|
}
|
|
|
|
// Handle drag and drop
|
|
['dragenter', 'dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
function highlight(e) {
|
|
dropArea.classList.add('highlight');
|
|
}
|
|
|
|
function unhighlight(e) {
|
|
dropArea.classList.remove('highlight');
|
|
}
|
|
|
|
// Handle drop
|
|
dropArea.addEventListener('drop', handleDrop, false);
|
|
|
|
function handleDrop(e) {
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
if (files.length > 0) {
|
|
handleFileSelect(files[0]);
|
|
}
|
|
}
|
|
|
|
// Handle drag and drop
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function highlight() {
|
|
dropArea.classList.add('dragover');
|
|
}
|
|
|
|
function unhighlight() {
|
|
dropArea.classList.remove('dragover');
|
|
}
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
dropArea.addEventListener('drop', function(e) {
|
|
const dt = e.dataTransfer;
|
|
const file = dt.files[0];
|
|
if (file) {
|
|
handleFileSelect(file);
|
|
}
|
|
});
|
|
|
|
// Set up save button
|
|
const saveBannerBtn = document.getElementById('saveBannerBtn');
|
|
if (saveBannerBtn) {
|
|
saveBannerBtn.addEventListener('click', saveBanner);
|
|
}
|
|
|
|
// Set up color input listeners
|
|
setupColorInputListeners();
|
|
|
|
// Initial preview update
|
|
updateBannerPreview();
|
|
|
|
// Load existing banner data
|
|
loadBanner();
|
|
});
|
|
|
|
// Prevent default drag behaviors
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Handle image upload
|
|
function handleImageUpload(event) {
|
|
const fileInput = event.target;
|
|
const file = fileInput.files[0];
|
|
|
|
if (!file) return;
|
|
|
|
// Check file type
|
|
const validImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
|
if (!validImageTypes.includes(file.type)) {
|
|
showNotification('Vyberte prosím soubor obrázku (JPG, PNG, GIF, SVG)', 'warning');
|
|
fileInput.value = ''; // Reset file input
|
|
return;
|
|
}
|
|
|
|
// Check file size (max 5MB)
|
|
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
if (file.size > maxSize) {
|
|
showNotification('Maximální velikost souboru je 5MB', 'warning');
|
|
fileInput.value = ''; // Reset file input
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
const previewContainer = document.getElementById('imagePreviewContainer');
|
|
const dragDropMessage = document.querySelector('.drag-drop-message');
|
|
const bannerPreview = document.getElementById('bannerPreview');
|
|
|
|
if (previewContainer) {
|
|
previewContainer.style.display = 'block';
|
|
previewContainer.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Načítání...</span></div></div>';
|
|
}
|
|
|
|
// Hide the drag & drop message
|
|
if (dragDropMessage) {
|
|
dragDropMessage.style.display = 'none';
|
|
}
|
|
|
|
// Process the image
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
// Update the image preview
|
|
const bannerImagePreview = document.getElementById('bannerImagePreview');
|
|
if (bannerImagePreview) {
|
|
bannerImagePreview.src = e.target.result;
|
|
bannerImagePreview.style.display = 'block';
|
|
bannerImagePreview.classList.remove('d-none');
|
|
|
|
// Show remove button
|
|
const removeBtn = document.getElementById('removeImageBtn');
|
|
if (removeBtn) removeBtn.style.display = 'inline-block';
|
|
|
|
// Update the current image
|
|
currentImage = e.target.result;
|
|
|
|
// Update the banner preview
|
|
updateBannerPreview();
|
|
|
|
// Show the preview container
|
|
if (previewContainer) {
|
|
previewContainer.style.display = 'block';
|
|
previewContainer.innerHTML = '';
|
|
previewContainer.appendChild(bannerImagePreview);
|
|
}
|
|
}
|
|
|
|
// Hide loading container
|
|
if (previewContainer) {
|
|
previewContainer.style.display = 'none';
|
|
}
|
|
|
|
// Show templates section if it exists
|
|
const bannerTemplates = document.getElementById('bannerTemplates');
|
|
if (bannerTemplates) {
|
|
bannerTemplates.style.display = 'block';
|
|
}
|
|
|
|
// Update banner preview with the new image
|
|
updateBannerPreview();
|
|
};
|
|
|
|
reader.onerror = function() {
|
|
showNotification('Při načítání obrázku došlo k chybě. Zkuste to prosím znovu.', 'error');
|
|
fileInput.value = ''; // Reset file input
|
|
|
|
// Reset preview
|
|
if (previewContainer) {
|
|
previewContainer.innerHTML = '';
|
|
previewContainer.style.display = 'none';
|
|
}
|
|
|
|
// Show drag & drop message again
|
|
if (dragDropMessage) {
|
|
dragDropMessage.style.display = 'flex';
|
|
}
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
// Hardcoded apps data - should match the ones in index.html
|
|
const HARDCODED_APPS = [
|
|
{
|
|
id: 'hardcoded-car',
|
|
name: 'Záznam služebních jízd',
|
|
url: '/evidence-aut',
|
|
description: 'Jednoduchý systém pro evidenci a správu jízd služebními vozidly.',
|
|
icon: 'fa-car-side',
|
|
color: 'blue'
|
|
},
|
|
{
|
|
id: 'hardcoded-lunch',
|
|
name: 'Objednávka obědů',
|
|
url: 'http://ppc-app/pwkweb2/',
|
|
description: 'Portál pro objednávku a přehled firemních obědů',
|
|
icon: 'fa-utensils',
|
|
color: 'green'
|
|
},
|
|
{
|
|
id: 'hardcoded-osticket',
|
|
name: 'OSTicket',
|
|
url: 'http://osticket/',
|
|
description: 'Systém technické podpory a hlášení problémů',
|
|
icon: 'fa-headset',
|
|
color: 'orange'
|
|
},
|
|
{
|
|
id: 'hardcoded-kanboard',
|
|
name: 'Kanboard',
|
|
url: 'http://kanboard/',
|
|
description: 'Správa úkolů a projektů v přehledném kanban stylu',
|
|
icon: 'fa-tasks',
|
|
color: 'purple'
|
|
}
|
|
];
|
|
|
|
// Load hardcoded apps
|
|
function loadHardcodedApps() {
|
|
const hardcodedAppsList = document.getElementById('hardcodedAppsList');
|
|
|
|
if (HARDCODED_APPS.length === 0) {
|
|
hardcodedAppsList.innerHTML = `
|
|
<div class="text-center py-4 text-gray-500">
|
|
Žádné přednastavené aplikace nebyly nalezeny
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
hardcodedAppsList.innerHTML = HARDCODED_APPS.map(app => `
|
|
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="w-12 h-12 rounded-full bg-${app.color}-100 text-${app.color}-600 flex items-center justify-center">
|
|
<i class="fas ${app.icon} text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium">${app.name}</h4>
|
|
<p class="text-sm text-gray-500">${app.url}</p>
|
|
<p class="text-sm text-gray-400">${app.description}</p>
|
|
</div>
|
|
</div>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
Přednastaveno
|
|
</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Load dynamic apps
|
|
async function loadDynamicApps() {
|
|
console.log("Loading dynamic apps...");
|
|
const dynamicAppsContainer = document.getElementById('dynamicApps');
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/apps', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
// Token expired or invalid, redirect to login
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const apps = await response.json();
|
|
console.log("Loaded dynamic apps:", apps);
|
|
|
|
if (!Array.isArray(apps) || apps.length === 0) {
|
|
dynamicAppsList.innerHTML = `
|
|
<div class="text-center py-8">
|
|
<i class="fas fa-inbox text-4xl text-gray-300 mb-2"></i>
|
|
<p class="text-gray-500">Žádné vlastní aplikace nebyly nalezeny</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Map all apps to HTML, including hardcoded ones
|
|
const dynamicApps = apps.map(app => `
|
|
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between ${app.id && app.id.startsWith('hardcoded-') ? 'opacity-75' : ''}" data-app-id="${app.id}">
|
|
<div class="flex items-center space-x-4">
|
|
${app.icon ?
|
|
`<img src="/uploads/${app.icon}" alt="${app.name}" class="w-12 h-12 object-contain">` :
|
|
`<div class="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
|
|
<i class="fas fa-apple-alt text-gray-400 text-xl"></i>
|
|
</div>`
|
|
}
|
|
<div>
|
|
<h4 class="font-medium">${app.name || 'Neznámá aplikace'}</h4>
|
|
<p class="text-sm text-gray-500">${app.url || ''}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button onclick="editApp('${app.id}')" class="text-blue-500 hover:text-blue-700">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button onclick="deleteApp('${app.id}')" class="text-red-500 hover:text-red-700">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
if (dynamicApps.length > 0) {
|
|
dynamicAppsList.innerHTML = dynamicApps;
|
|
} else {
|
|
dynamicAppsList.innerHTML = `
|
|
<div class="text-center py-8">
|
|
<i class="fas fa-inbox text-4xl text-gray-300 mb-2"></i>
|
|
<p class="text-gray-500">Žádné vlastní aplikace nebyly nalezeny</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading dynamic apps:', error);
|
|
dynamicAppsList.innerHTML = `
|
|
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-exclamation-circle text-red-400"></i>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm text-red-700">
|
|
Chyba při načítání aplikací: ${error.message}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Load all apps (both hardcoded and dynamic)
|
|
async function loadApps() {
|
|
loadHardcodedApps();
|
|
await loadDynamicApps();
|
|
}
|
|
|
|
async function saveApp(event) {
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
const formData = new FormData();
|
|
const appId = document.getElementById('appId').value;
|
|
|
|
// Basic validation
|
|
const name = document.getElementById('appName').value.trim();
|
|
const url = document.getElementById('appUrl').value.trim();
|
|
|
|
if (!name) {
|
|
showNotification('Název aplikace je povinný', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!url) {
|
|
showNotification('URL adresa je povinná', 'error');
|
|
return;
|
|
}
|
|
|
|
formData.append('name', name);
|
|
formData.append('url', url);
|
|
formData.append('description', document.getElementById('appDescription').value.trim());
|
|
|
|
// Handle icon upload if a new file is selected
|
|
const iconInput = document.getElementById('appIcon');
|
|
if (iconInput.files.length > 0) {
|
|
formData.append('icon', iconInput.files[0]);
|
|
}
|
|
|
|
try {
|
|
const isEdit = !!appId;
|
|
const url = isEdit ? `/api/apps/${appId}` : '/api/apps';
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
|
|
// Show loading state
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const originalBtnText = submitBtn.innerHTML;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Ukládám...';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
body: formData,
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
// Don't set Content-Type header when using FormData, let the browser set it with the correct boundary
|
|
}
|
|
});
|
|
|
|
// Reset button state
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalBtnText;
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = 'Nepodařilo se uložit aplikaci';
|
|
try {
|
|
const errorData = await response.json();
|
|
errorMessage = errorData.message || errorMessage;
|
|
} catch (e) {
|
|
console.error('Error parsing error response:', e);
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
closeAppModal();
|
|
|
|
// Reload only dynamic apps (faster than reloading everything)
|
|
await loadDynamicApps();
|
|
|
|
showNotification(
|
|
`Aplikace byla úspěšně ${isEdit ? 'aktualizována' : 'vytvořena'}`,
|
|
'success'
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error('Chyba při ukládání aplikace:', error);
|
|
showNotification(
|
|
error.message || 'Došlo k chybě při ukládání aplikace',
|
|
'error'
|
|
);
|
|
}
|
|
}
|
|
|
|
async function editApp(appId) {
|
|
// Prevent editing hardcoded apps
|
|
if (appId && appId.startsWith('hardcoded-')) {
|
|
showNotification('Tuto přednastavenou aplikaci nelze upravit', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/apps/${appId}`);
|
|
if (!response.ok) throw new Error('Nepodařilo se načíst data aplikace');
|
|
|
|
const app = await response.json();
|
|
|
|
// Set form values
|
|
document.getElementById('appId').value = app.id;
|
|
document.getElementById('appName').value = app.name;
|
|
document.getElementById('appUrl').value = app.url;
|
|
document.getElementById('appDescription').value = app.description || '';
|
|
document.getElementById('appModalTitle').textContent = 'Upravit aplikaci';
|
|
|
|
// Update file name display
|
|
const fileNameDisplay = document.getElementById('fileName');
|
|
if (fileNameDisplay) {
|
|
fileNameDisplay.textContent = app.icon ? 'Stávající soubor: ' + app.icon : 'Nebyl vybrán žádný soubor';
|
|
}
|
|
|
|
// Show icon preview if exists
|
|
const iconPreview = document.getElementById('appIconPreview');
|
|
if (app.icon) {
|
|
iconPreview.src = `/uploads/${app.icon}`;
|
|
iconPreview.classList.remove('hidden');
|
|
} else {
|
|
iconPreview.classList.add('hidden');
|
|
}
|
|
|
|
// Clear file input to allow re-selecting the same file
|
|
const fileInput = document.getElementById('appIcon');
|
|
if (fileInput) {
|
|
fileInput.value = '';
|
|
}
|
|
|
|
// Show the modal
|
|
document.getElementById('appModal').classList.remove('hidden');
|
|
|
|
} catch (error) {
|
|
console.error('Chyba při načítání aplikace:', error);
|
|
showNotification('Nepodařilo se načíst data aplikace', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteApp(appId) {
|
|
// Prevent deleting hardcoded apps
|
|
if (appId && appId.startsWith('hardcoded-')) {
|
|
showNotification('Tuto přednastavenou aplikaci nelze smazat', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('Opravdu chcete tuto aplikaci smazat? Tato akce je nevratná.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/apps/${appId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || 'Nepodařilo se smazat aplikaci');
|
|
}
|
|
|
|
// Reload only dynamic apps
|
|
await loadDynamicApps();
|
|
showNotification('Aplikace byla úspěšně smazána', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Chyba při mazání aplikace:', error);
|
|
showNotification(error.message || 'Nepodařilo se smazat aplikaci', 'error');
|
|
}
|
|
}
|
|
|
|
function openAddAppModal() {
|
|
// Reset form
|
|
const form = document.getElementById('appForm');
|
|
if (form) form.reset();
|
|
|
|
// Clear any existing ID
|
|
document.getElementById('appId').value = '';
|
|
|
|
// Update title
|
|
document.getElementById('appModalTitle').textContent = 'Přidat aplikaci';
|
|
|
|
// Reset file input and preview
|
|
const fileInput = document.getElementById('appIcon');
|
|
const fileNameDisplay = document.getElementById('fileName');
|
|
const previewImg = document.getElementById('appIconPreview');
|
|
|
|
if (fileInput) fileInput.value = '';
|
|
if (fileNameDisplay) fileNameDisplay.textContent = 'Nebyl vybrán žádný soubor';
|
|
if (previewImg) {
|
|
previewImg.src = '';
|
|
previewImg.classList.add('hidden');
|
|
}
|
|
|
|
// Show the modal
|
|
document.getElementById('appModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeAppModal() {
|
|
document.getElementById('appModal').classList.add('hidden');
|
|
}
|
|
|
|
// Handle file input change and preview
|
|
function setupFileInput() {
|
|
const fileInput = document.getElementById('appIcon');
|
|
const fileNameDisplay = document.getElementById('fileName');
|
|
const previewImg = document.getElementById('appIconPreview');
|
|
|
|
if (!fileInput || !fileNameDisplay || !previewImg) return;
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
|
|
if (file) {
|
|
// Update file name display
|
|
fileNameDisplay.textContent = file.name;
|
|
|
|
// Show preview if it's an image
|
|
if (file.type.startsWith('image/')) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
previewImg.src = e.target.result;
|
|
previewImg.classList.remove('hidden');
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
previewImg.classList.add('hidden');
|
|
}
|
|
} else {
|
|
fileNameDisplay.textContent = 'Nebyl vybrán žádný soubor';
|
|
previewImg.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Reset form when modal is closed
|
|
document.getElementById('appModal').addEventListener('hidden.bs.modal', function () {
|
|
const form = document.getElementById('appForm');
|
|
if (form) form.reset();
|
|
document.getElementById('appId').value = '';
|
|
const previewImg = document.getElementById('appIconPreview');
|
|
if (previewImg) {
|
|
previewImg.classList.add('hidden');
|
|
previewImg.src = '';
|
|
}
|
|
const fileNameDisplay = document.getElementById('fileName');
|
|
if (fileNameDisplay) {
|
|
fileNameDisplay.textContent = 'Nebyl vybrán žádný soubor';
|
|
}
|
|
});
|
|
|
|
// Initialize file input handling when the page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setupFileInput();
|
|
});
|
|
|
|
// Logout functionality
|
|
document.getElementById('logoutBtn').addEventListener('click', function() {
|
|
localStorage.removeItem('token');
|
|
window.location.href = '/';
|
|
});
|
|
|
|
// DOM Elements
|
|
let bannerText, bannerVisible, bannerBgColor, bannerTextColor, bannerTextAlign, bannerFontSize,
|
|
bannerPadding, bannerMargin, bannerBorderRadius, bannerPreview, bannerPreviewContent,
|
|
bannerPreviewText, bannerPreviewBg, bgColorPreview, textColorPreview, saveBannerBtn,
|
|
stylePresets, currentImage = null, currentTemplate = 'modern-minimal';
|
|
|
|
// Preset styles
|
|
const presets = {
|
|
info: {
|
|
backgroundColor: '#cce5ff',
|
|
textColor: '#004085',
|
|
textAlign: 'left'
|
|
},
|
|
warning: {
|
|
backgroundColor: '#fff3cd',
|
|
textColor: '#856404',
|
|
textAlign: 'center'
|
|
},
|
|
success: {
|
|
backgroundColor: '#d4edda',
|
|
textColor: '#155724',
|
|
textAlign: 'center'
|
|
},
|
|
error: {
|
|
backgroundColor: '#f8d7da',
|
|
textColor: '#721c24',
|
|
textAlign: 'center'
|
|
}
|
|
};
|
|
|
|
// Variables for image positioning
|
|
let currentImagePosition = 'right'; // default position
|
|
let currentImageX = '0';
|
|
let currentImageY = '0';
|
|
|
|
// Load banner data
|
|
async function loadBanner() {
|
|
try {
|
|
const response = await fetch('/api/banner');
|
|
if (!response.ok) throw new Error('Nepodařilo se načíst banner');
|
|
|
|
const data = await response.json();
|
|
console.log('Loaded banner data:', data);
|
|
|
|
if (data) {
|
|
// Update form fields
|
|
const bannerText = document.getElementById('bannerText');
|
|
const bannerLink = document.getElementById('bannerLink');
|
|
const bannerVisible = document.getElementById('bannerVisibility');
|
|
|
|
if (bannerText) bannerText.value = data.Text || '';
|
|
if (bannerLink) bannerLink.value = data.Link || '';
|
|
if (bannerVisible) {
|
|
bannerVisible.checked = data.Style?.IsVisible !== false;
|
|
// Update the hidden input for form submission
|
|
const hiddenVisible = document.getElementById('bannerVisible');
|
|
if (hiddenVisible) {
|
|
hiddenVisible.value = bannerVisible.checked ? 'true' : 'false';
|
|
}
|
|
// Force update visibility in preview
|
|
updateBannerPreview();
|
|
}
|
|
|
|
// Initialize image position variables once
|
|
currentImagePosition = data.Style?.ImagePosition || 'right';
|
|
currentImageX = data.Style?.ImageX || '0';
|
|
currentImageY = data.Style?.ImageY || '0';
|
|
|
|
// Update position buttons to reflect the loaded position
|
|
const positionButtons = document.querySelectorAll('.position-btn');
|
|
if (positionButtons.length > 0) {
|
|
positionButtons.forEach(button => {
|
|
if (button.dataset.position === currentImagePosition) {
|
|
button.classList.add('active');
|
|
} else {
|
|
button.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Apply the saved template if it exists
|
|
if (data.Style?.template && templateConfigs[data.Style.template]) {
|
|
// Apply the template
|
|
applyTemplate(data.Style.template);
|
|
|
|
// Update the active template in the UI
|
|
const templateItems = document.querySelectorAll('.template-item');
|
|
if (templateItems) {
|
|
templateItems.forEach(item => {
|
|
if (item && item.dataset.template === data.Style.template) {
|
|
item.classList.add('active');
|
|
} else if (item) {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Fallback to default template if none is set
|
|
applyTemplate('modern-minimal');
|
|
}
|
|
|
|
// Handle image
|
|
const bannerImgElement = document.getElementById('bannerImagePreview');
|
|
const uploadPrompt = document.getElementById('uploadPrompt');
|
|
const imagePreview = document.getElementById('imagePreview');
|
|
|
|
if (data.Image) {
|
|
currentImage = data.Image;
|
|
|
|
if (bannerImgElement) {
|
|
bannerImgElement.src = data.Image;
|
|
const bannerLinkValue = data.Link || '';
|
|
|
|
if (bannerLinkValue) {
|
|
bannerImgElement.style.cursor = 'pointer';
|
|
bannerImgElement.onclick = (e) => {
|
|
e.preventDefault();
|
|
window.open(bannerLinkValue, '_blank');
|
|
};
|
|
} else {
|
|
bannerImgElement.style.cursor = 'default';
|
|
bannerImgElement.onclick = null;
|
|
}
|
|
|
|
// Show the image preview and hide the upload prompt
|
|
if (uploadPrompt) uploadPrompt.style.display = 'none';
|
|
if (imagePreview) imagePreview.style.display = 'block';
|
|
}
|
|
|
|
// Update position if exists
|
|
if (data.Style?.ImagePosition) {
|
|
currentImagePosition = data.Style.ImagePosition;
|
|
|
|
// Update active state of position buttons
|
|
const positionButtons = document.querySelectorAll('.position-btn');
|
|
if (positionButtons) {
|
|
positionButtons.forEach(btn => {
|
|
if (btn) {
|
|
btn.classList.remove('active', 'btn-primary');
|
|
btn.classList.add('btn-outline-secondary');
|
|
if (btn.dataset.position === currentImagePosition) {
|
|
btn.classList.add('active', 'btn-primary');
|
|
btn.classList.remove('btn-outline-secondary');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update coordinates if they exist
|
|
if (data.Style?.ImageX !== undefined && data.Style?.ImageY !== undefined) {
|
|
currentImageX = data.Style.ImageX;
|
|
currentImageY = data.Style.ImageY;
|
|
|
|
// Update image preview position if it exists
|
|
if (bannerImgElement) {
|
|
bannerImgElement.style.left = `${currentImageX}px`;
|
|
bannerImgElement.style.top = `${currentImageY}px`;
|
|
|
|
// Activate custom position button if it exists
|
|
const customPosBtn = document.getElementById('customPosBtn');
|
|
if (customPosBtn) {
|
|
customPosBtn.classList.add('active', 'btn-primary');
|
|
customPosBtn.classList.remove('btn-outline-secondary');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show remove button if it exists
|
|
const removeBtn = document.getElementById('removeImageBtn');
|
|
if (removeBtn) {
|
|
removeBtn.style.display = 'block';
|
|
}
|
|
|
|
// Set remove image input if it exists
|
|
const removeImageInput = document.getElementById('removeImage');
|
|
if (removeImageInput) {
|
|
removeImageInput.value = 'false';
|
|
}
|
|
} else {
|
|
// No image in the saved banner
|
|
currentImage = null;
|
|
|
|
// Hide image preview and show upload prompt
|
|
if (imagePreview) {
|
|
imagePreview.style.display = 'none';
|
|
}
|
|
if (uploadPrompt) {
|
|
uploadPrompt.style.display = 'block';
|
|
}
|
|
|
|
// Hide remove button
|
|
const removeBtn = document.getElementById('removeImageBtn');
|
|
if (removeBtn) {
|
|
removeBtn.style.display = 'none';
|
|
}
|
|
|
|
// Set remove image input
|
|
const removeImageInput = document.getElementById('removeImage');
|
|
if (removeImageInput) {
|
|
removeImageInput.value = 'true';
|
|
}
|
|
}
|
|
|
|
// Update previews
|
|
updateColorPreviews();
|
|
updateBannerPreview();
|
|
}
|
|
} catch (error) {
|
|
console.error('Chyba při načítání banneru:', error);
|
|
showNotification('Chyba při načítání banneru', 'error');
|
|
}
|
|
}
|
|
|
|
// Add submission flag at the top of the script
|
|
let isSubmitting = false;
|
|
|
|
async function saveBanner(event) {
|
|
event.preventDefault();
|
|
|
|
// Prevent multiple submissions
|
|
if (isSubmitting) {
|
|
console.log('Form submission already in progress');
|
|
return;
|
|
}
|
|
|
|
isSubmitting = true;
|
|
|
|
const form = document.getElementById('bannerForm');
|
|
const formData = new FormData(form);
|
|
const submitButton = form.querySelector('button[type="submit"]');
|
|
const originalButtonText = submitButton ? submitButton.innerHTML : '';
|
|
|
|
try {
|
|
// Show loading state
|
|
if (submitButton) {
|
|
submitButton.disabled = true;
|
|
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Ukládám...';
|
|
}
|
|
|
|
// Add text and link to form data
|
|
const bannerText = document.getElementById('bannerText');
|
|
const bannerLink = document.getElementById('bannerLink');
|
|
const bannerVisibility = document.getElementById('bannerVisibility');
|
|
const bannerVisible = document.getElementById('bannerVisible');
|
|
|
|
// Update hidden field based on checkbox state
|
|
if (bannerVisibility) {
|
|
bannerVisible.value = bannerVisibility.checked ? 'true' : 'false';
|
|
}
|
|
|
|
// Get HTML content from the contenteditable div
|
|
const bannerTextContent = bannerText ? bannerText.innerHTML : '';
|
|
|
|
// Add banner content with proper field names
|
|
formData.append('Text', bannerTextContent);
|
|
formData.append('Link', bannerLink ? bannerLink.value : '');
|
|
formData.append('IsVisible', bannerVisible ? bannerVisible.value : 'true');
|
|
|
|
// Add style values with proper field names
|
|
formData.append('Style[BackgroundColor]', bannerBgColorPicker?.value || '');
|
|
formData.append('Style[TextColor]', bannerTextColorPicker?.value || '');
|
|
formData.append('Style[TextAlign]', bannerTextAlign?.value || 'left');
|
|
formData.append('Style[FontSize]', bannerFontSize?.value || '16');
|
|
formData.append('Style[Padding]', bannerPadding?.value || '20');
|
|
formData.append('Style[Margin]', bannerMargin?.value || '20');
|
|
formData.append('Style[BorderRadius]', bannerBorderRadius?.value || '8');
|
|
formData.append('Style[IsVisible]', bannerVisible ? bannerVisible.value : 'true');
|
|
formData.append('Style[ImagePosition]', currentImagePosition || 'right');
|
|
formData.append('Style[ImageX]', currentImageX || '0');
|
|
formData.append('Style[ImageY]', currentImageY || '0');
|
|
formData.append('Style[ImagePosition]', currentImagePosition || 'right');
|
|
formData.append('Style[ImageX]', currentImageX || '0');
|
|
formData.append('Style[ImageY]', currentImageY || '0');
|
|
|
|
// Add template styles if available
|
|
if (currentTemplate && templateConfigs[currentTemplate]) {
|
|
const template = templateConfigs[currentTemplate];
|
|
if (template.backgroundColor) formData.append('Style[BackgroundColor]', template.backgroundColor);
|
|
if (template.textColor) formData.append('Style[TextColor]', template.textColor);
|
|
if (template.textAlign) formData.append('Style[TextAlign]', template.textAlign);
|
|
if (template.fontSize) formData.append('Style[FontSize]', template.fontSize);
|
|
if (template.padding) formData.append('Style[Padding]', template.padding);
|
|
if (template.margin) formData.append('Style[Margin]', template.margin);
|
|
if (template.borderRadius) formData.append('Style[BorderRadius]', template.borderRadius);
|
|
if (template.background) formData.append('Style[Background]', template.background);
|
|
if (template.containerStyle) formData.append('Style[ContainerStyle]', template.containerStyle);
|
|
}
|
|
|
|
// Ensure templateConfigs is defined and has the default template
|
|
if (typeof templateConfigs === 'undefined') {
|
|
templateConfigs = {};
|
|
}
|
|
|
|
// Get the current template or use default
|
|
const defaultTemplate = templateConfigs['modern-minimal'] || {
|
|
backgroundColor: '#f8f9fa',
|
|
textColor: '#212529',
|
|
textAlign: 'left',
|
|
fontSize: 16,
|
|
padding: 20,
|
|
margin: 20,
|
|
borderRadius: 8
|
|
};
|
|
|
|
const template = (currentTemplate && templateConfigs[currentTemplate]) || defaultTemplate;
|
|
|
|
// Add template name
|
|
formData.append('template', currentTemplate || 'modern-minimal');
|
|
|
|
// Get custom values if they exist
|
|
const customBg = bannerBgColorPicker?.value || '';
|
|
const customTextColor = bannerTextColorPicker?.value || '';
|
|
const imgPosition = document.getElementById('bannerImagePosition')?.value || 'right';
|
|
|
|
// Apply all template styles with proper fallbacks
|
|
const styles = {
|
|
// Background styles
|
|
background: (template.background || '').trim(),
|
|
backgroundColor: (customBg || template.backgroundColor || defaultTemplate.backgroundColor).trim(),
|
|
|
|
// Text styles
|
|
color: (customTextColor || template.textColor || defaultTemplate.textColor).trim(),
|
|
textColor: (customTextColor || template.textColor || defaultTemplate.textColor).trim(),
|
|
textAlign: (template.textAlign || defaultTemplate.textAlign).trim(),
|
|
fontSize: template.fontSize ? `${template.fontSize}px` : `${defaultTemplate.fontSize}px`,
|
|
|
|
// Layout styles
|
|
padding: template.padding ? `${template.padding}px` : `${defaultTemplate.padding}px`,
|
|
margin: template.margin ? `${template.margin}px` : `${defaultTemplate.margin}px`,
|
|
borderRadius: template.borderRadius ? `${template.borderRadius}px` : `${defaultTemplate.borderRadius}px`,
|
|
|
|
// Image position
|
|
imagePosition: imgPosition,
|
|
|
|
// Button styles (if template defines them)
|
|
buttonBackground: (template.buttonBackground || '#4a6cf7').trim(),
|
|
buttonTextColor: (template.buttonTextColor || '#ffffff').trim(),
|
|
buttonBorder: (template.buttonBorder || 'none').trim(),
|
|
|
|
// Container styles
|
|
containerStyle: (template.containerStyle || '').trim()
|
|
};
|
|
|
|
// Special handling for specific templates
|
|
if (currentTemplate === 'dark') {
|
|
styles.background = '#2d3748';
|
|
styles.backgroundColor = '#2d3748';
|
|
styles.color = '#e2e8f0';
|
|
styles.textColor = '#e2e8f0';
|
|
}
|
|
|
|
// Add all styles to form data
|
|
Object.entries(styles).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== '') {
|
|
formData.append(`style[${key}]`, value);
|
|
}
|
|
});
|
|
|
|
// Add image position
|
|
const imagePosition = document.querySelector('.image-pos-btn.active')?.dataset.pos || 'center';
|
|
formData.append('style[imagePosition]', imagePosition);
|
|
|
|
// Add custom position if needed
|
|
if (imagePosition === 'custom') {
|
|
formData.append('style[imageX]', currentImageX || '0');
|
|
formData.append('style[imageY]', currentImageY || '0');
|
|
}
|
|
|
|
// Add image dimensions
|
|
const imageWidth = document.getElementById('bannerImageWidth');
|
|
const imageHeight = document.getElementById('bannerImageHeight');
|
|
|
|
if (imageWidth && imageHeight) {
|
|
formData.append('imageWidth', imageWidth.value || '300');
|
|
formData.append('imageHeight', imageHeight.value || '200');
|
|
formData.append('style[imageWidth]', imageWidth.value || '300');
|
|
formData.append('style[imageHeight]', imageHeight.value || '200');
|
|
}
|
|
|
|
// Handle image upload if a new image was selected
|
|
const fileInput = document.getElementById('bannerImage');
|
|
if (fileInput && fileInput.files && fileInput.files.length > 0) {
|
|
formData.append('image', fileInput.files[0]);
|
|
} else if (currentImage && currentImage.startsWith('data:image')) {
|
|
try {
|
|
// If we have a data URL but no file input, convert it to a blob
|
|
const response = await fetch(currentImage);
|
|
if (!response.ok) throw new Error('Failed to fetch image');
|
|
const blob = await response.blob();
|
|
formData.append('image', blob, 'banner-image.jpg');
|
|
} catch (error) {
|
|
console.error('Error processing image:', error);
|
|
showNotification('Chyba při zpracování obrázku', 'error');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Log form data for debugging (without the actual file data)
|
|
console.log('Odesílám data:');
|
|
for (let [key, value] of formData.entries()) {
|
|
console.log(key, value instanceof File ? `[File ${value.name}]` : value);
|
|
}
|
|
|
|
// Prepare headers
|
|
const headers = {};
|
|
|
|
// Add Authorization header if token exists
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
// Send request with FormData (browser will set correct Content-Type with boundary)
|
|
const response = await fetch('/api/banner/update', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Neznámá chyba serveru');
|
|
console.error('Server error:', errorText);
|
|
let errorMessage = 'Chyba při ukládání banneru';
|
|
|
|
try {
|
|
const errorData = JSON.parse(errorText);
|
|
errorMessage = errorData.message || errorMessage;
|
|
} catch (e) {
|
|
errorMessage = errorText || errorMessage;
|
|
}
|
|
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
const result = await response.json().catch(() => ({}));
|
|
|
|
// Show success message
|
|
showNotification('Banner byl úspěšně uložen', 'success');
|
|
|
|
// Update the preview with the new banner data
|
|
if (result.imageUrl) {
|
|
currentImage = result.imageUrl;
|
|
const imagePreview = document.getElementById('imagePreview');
|
|
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
|
|
const removeBtn = document.getElementById('removeImageBtn');
|
|
|
|
if (imagePreview) imagePreview.src = currentImage;
|
|
if (imagePreviewContainer) imagePreviewContainer.style.display = 'block';
|
|
if (removeBtn) removeBtn.style.display = 'inline-block';
|
|
|
|
// Update the hidden input if the image was changed
|
|
const removeImageInput = document.getElementById('removeImage');
|
|
if (removeImageInput) removeImageInput.value = 'false';
|
|
|
|
// Show templates and size controls since we have an image
|
|
const bannerTemplates = document.getElementById('bannerTemplates');
|
|
const imageSizeControls = document.getElementById('imageSizeControls');
|
|
if (bannerTemplates) bannerTemplates.style.display = 'block';
|
|
if (imageSizeControls) imageSizeControls.style.display = 'flex';
|
|
}
|
|
|
|
// Update the preview
|
|
updateBannerPreview();
|
|
|
|
} catch (error) {
|
|
console.error('Chyba při ukládání banneru:', error);
|
|
showNotification(error.message || 'Nepodařilo se uložit banner', 'error');
|
|
|
|
} finally {
|
|
// Reset button state
|
|
if (submitButton) {
|
|
submitButton.disabled = false;
|
|
submitButton.innerHTML = originalButtonText;
|
|
}
|
|
isSubmitting = false;
|
|
}
|
|
}
|
|
|
|
// Update color previews
|
|
function updateColorPreviews() {
|
|
const bgColorPreview = document.getElementById('bgColorPreview');
|
|
const textColorPreview = document.getElementById('textColorPreview');
|
|
const bgColorPicker = document.getElementById('bannerBgColorPicker');
|
|
const textColorPicker = document.getElementById('bannerTextColorPicker');
|
|
const bgColorInput = document.getElementById('bannerBgColor');
|
|
const textColorInput = document.getElementById('bannerTextColor');
|
|
|
|
if (bgColorPreview && bgColorInput) {
|
|
bgColorPreview.style.backgroundColor = bgColorInput.value;
|
|
}
|
|
|
|
if (textColorPreview && textColorInput) {
|
|
textColorPreview.style.backgroundColor = textColorInput.value;
|
|
}
|
|
|
|
if (bgColorPicker && bgColorInput) {
|
|
bgColorPicker.value = bgColorInput.value;
|
|
}
|
|
|
|
if (textColorPicker && textColorInput) {
|
|
textColorPicker.value = textColorInput.value;
|
|
}
|
|
}
|
|
|
|
// Remove image
|
|
function removeImage() {
|
|
const bannerImageInput = document.getElementById('bannerImage');
|
|
const bannerImagePreview = document.getElementById('bannerImagePreview');
|
|
const removeBtn = document.getElementById('removeImageBtn');
|
|
const dragDropMessage = document.querySelector('.drag-drop-message');
|
|
const previewContainer = document.getElementById('imagePreviewContainer');
|
|
|
|
// Reset file input
|
|
if (bannerImageInput) {
|
|
bannerImageInput.value = '';
|
|
}
|
|
|
|
// Clear current image
|
|
currentImage = null;
|
|
|
|
// Update banner preview
|
|
updateBannerPreview();
|
|
|
|
// Show message
|
|
showNotification('Obrázek byl odstraněn', 'info');
|
|
|
|
// Trigger change event on the file input
|
|
const bannerImage = document.getElementById('bannerImage');
|
|
if (bannerImage) {
|
|
const event = new Event('change');
|
|
bannerImage.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
// Update banner preview
|
|
function updateBannerPreview() {
|
|
const bannerPreview = document.getElementById('bannerPreview');
|
|
const bannerPreviewContent = document.getElementById('bannerPreviewContent');
|
|
const bannerText = document.getElementById('bannerText')?.value || '';
|
|
const bannerVisible = document.getElementById('bannerVisible')?.checked !== false;
|
|
const bannerTemplates = document.getElementById('bannerTemplates');
|
|
const imagePreview = document.getElementById('imagePreview');
|
|
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
|
|
const imageSizeControls = document.getElementById('imageSizeControls');
|
|
|
|
// Get the current template config or use default
|
|
const defaultTemplate = templateConfigs['modern-minimal'] || {};
|
|
const template = currentTemplate ? (templateConfigs[currentTemplate] || defaultTemplate) : defaultTemplate;
|
|
const fileInput = document.getElementById('bannerImage');
|
|
const hasImage = Boolean(currentImage || (fileInput && fileInput.files && fileInput.files.length > 0));
|
|
|
|
// Ensure template has required properties
|
|
if (!template.background && !template.backgroundColor) {
|
|
template.backgroundColor = '#f8f9fa';
|
|
}
|
|
|
|
// Show/hide templates and size controls based on image presence
|
|
if (bannerTemplates) {
|
|
bannerTemplates.style.display = hasImage ? 'block' : 'none';
|
|
}
|
|
if (imageSizeControls) {
|
|
imageSizeControls.style.display = hasImage ? 'flex' : 'none';
|
|
}
|
|
|
|
// Create banner content based on template
|
|
let bannerContent = '';
|
|
const bannerLink = document.getElementById('bannerLink')?.value || '';
|
|
const bannerTextContent = bannerText || 'Náhled banneru';
|
|
|
|
if (hasImage && currentImage) {
|
|
// Get image dimensions
|
|
const imageWidth = parseInt(document.getElementById('bannerImageWidth')?.value || '300');
|
|
const imageHeight = parseInt(document.getElementById('bannerImageHeight')?.value || '200');
|
|
|
|
// Get the selected position or default to 'right'
|
|
const position = currentImagePosition || 'right';
|
|
|
|
// Set max dimensions based on position
|
|
const maxWidth = '30%';
|
|
const maxHeight = '300px';
|
|
|
|
// Determine flex direction and alignment based on position
|
|
const flexDirection = position === 'left' ? 'row' : 'row-reverse';
|
|
const alignSelf = 'flex-start';
|
|
const marginLeft = position === 'left' ? '0' : 'auto';
|
|
const marginRight = position === 'right' ? '0' : 'auto';
|
|
|
|
// Create flex container for the image
|
|
let imgContainer = `
|
|
<div class="banner-image-container" style="
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
max-width: ${maxWidth};
|
|
width: 30%;
|
|
justify-content: ${position === 'left' ? 'flex-start' : 'flex-end'};
|
|
align-self: ${alignSelf};
|
|
margin-left: ${marginLeft};
|
|
margin-right: ${marginRight};
|
|
order: ${position === 'left' ? 0 : 1};
|
|
">
|
|
<div style="
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
">
|
|
<img
|
|
src="${currentImage}"
|
|
style="
|
|
max-width: 100%;
|
|
max-height: ${maxHeight};
|
|
width: auto;
|
|
height: auto;
|
|
object-fit: contain;
|
|
border-radius: 8px;
|
|
display: block;
|
|
"
|
|
alt="Nahraný obrázek"
|
|
class="banner-image"
|
|
draggable="false"
|
|
>
|
|
</div>
|
|
</div>`;
|
|
|
|
// Wrap image with link if URL is provided
|
|
if (bannerLink) {
|
|
imgContainer = `
|
|
<a href="${bannerLink}" target="_blank" style="text-decoration: none;">
|
|
${imgContainer}
|
|
</a>`;
|
|
}
|
|
|
|
// Get all styles from template or use defaults
|
|
const styles = {
|
|
// Background styles
|
|
background: template.background || '',
|
|
backgroundColor: (bannerBgColorPicker?.value || template.backgroundColor || '#f8f9fa').trim(),
|
|
|
|
// Text styles
|
|
color: (bannerTextColorPicker?.value || template.textColor || '#212529').trim(),
|
|
textColor: (bannerTextColorPicker?.value || template.textColor || '#212529').trim(),
|
|
textAlign: (template.textAlign || 'left').trim(),
|
|
fontSize: template.fontSize ? `${template.fontSize}px` : '16px',
|
|
|
|
// Layout styles
|
|
padding: (template.padding ? `${template.padding}px` : '20px').trim(),
|
|
margin: (template.margin ? `${template.margin}px` : '20px').trim(),
|
|
borderRadius: (template.borderRadius ? `${template.borderRadius}px` : '8px').trim(),
|
|
|
|
// Image position (always right)
|
|
imagePosition: 'right',
|
|
|
|
// Container styles
|
|
containerStyle: (template.containerStyle || '').trim()
|
|
};
|
|
|
|
// Ensure background is properly set
|
|
if (!styles.background && styles.backgroundColor) {
|
|
styles.background = styles.backgroundColor;
|
|
}
|
|
|
|
// Create text content with template styles - only create the text element once
|
|
const textStyle = `
|
|
font-size: ${document.getElementById('bannerFontSize')?.value || '16'}px;
|
|
color: ${document.getElementById('bannerTextColor')?.value || '#000'};
|
|
text-align: ${document.getElementById('bannerTextAlign')?.value || 'left'};
|
|
margin: 0;
|
|
padding: 10px 0;
|
|
line-height: 1.5;
|
|
${template?.textStyle || ''}
|
|
`;
|
|
|
|
// Create the text element content
|
|
const textContent = `
|
|
<div class="banner-text" style="${textStyle}">
|
|
${bannerTextContent}
|
|
</div>`;
|
|
|
|
// Set the text element variable that will be used in the banner content
|
|
const textElement = textContent;
|
|
|
|
// Special handling for specific templates
|
|
if (currentTemplate === 'dark') {
|
|
styles.background = '#2d3748';
|
|
styles.backgroundColor = '#2d3748';
|
|
styles.color = '#e2e8f0';
|
|
styles.textColor = '#e2e8f0';
|
|
}
|
|
|
|
// Build the background style
|
|
let backgroundStyle = styles.background.includes('gradient')
|
|
? `background: ${styles.background};`
|
|
: `background-color: ${styles.backgroundColor};`;
|
|
|
|
// Create container with right-aligned image layout
|
|
bannerContent = `
|
|
<div class="banner-content" style="
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
padding: ${styles.padding};
|
|
margin: ${styles.margin};
|
|
background: ${styles.background || styles.backgroundColor};
|
|
color: ${styles.color};
|
|
border-radius: ${styles.borderRadius};
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
${styles.containerStyle};
|
|
gap: 20px;
|
|
width: 100%;
|
|
max-width: 1200px;
|
|
box-sizing: border-box;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
">
|
|
<div style="
|
|
flex: 1;
|
|
text-align: ${styles.textAlign};
|
|
font-size: ${styles.fontSize};
|
|
padding-right: 20px;
|
|
">
|
|
${textElement}
|
|
</div>
|
|
${imgContainer}
|
|
</div>`;
|
|
|
|
// Show the image preview in the container
|
|
try {
|
|
const bannerImagePreview = document.getElementById('bannerImagePreview');
|
|
if (bannerImagePreview && currentImage) {
|
|
bannerImagePreview.src = currentImage;
|
|
bannerImagePreview.style.width = '100%';
|
|
bannerImagePreview.style.height = 'auto';
|
|
bannerImagePreview.style.maxHeight = '200px';
|
|
bannerImagePreview.style.display = 'block';
|
|
bannerImagePreview.style.objectFit = 'contain';
|
|
bannerImagePreview.onerror = function() {
|
|
console.error('Failed to load banner image:', this.src);
|
|
this.style.display = 'none';
|
|
};
|
|
}
|
|
|
|
if (imagePreviewContainer) {
|
|
imagePreviewContainer.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating banner preview:', error);
|
|
}
|
|
|
|
// Add the with-image class to the banner preview for proper spacing
|
|
bannerPreview.classList.add('with-image');
|
|
} else {
|
|
// No image, just show text
|
|
bannerContent = `
|
|
<div class="banner-content" style="
|
|
${template?.containerStyle || ''}
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 30px;
|
|
min-height: 200px;
|
|
">
|
|
<div class="banner-text" style="
|
|
text-align: center;
|
|
color: #666;
|
|
font-style: italic;
|
|
${template?.textStyle || ''}
|
|
">
|
|
${bannerText || 'Náhled banneru (pro zobrazení šablon nahrajte obrázek)'}
|
|
</div>
|
|
</div>`;
|
|
|
|
// Hide image preview container if no image
|
|
if (imagePreviewContainer) {
|
|
imagePreviewContainer.style.display = 'none';
|
|
}
|
|
if (bannerPreview) {
|
|
bannerPreview.classList.remove('with-image');
|
|
}
|
|
}
|
|
|
|
// Apply template styles to the banner preview
|
|
if (template && bannerPreview) {
|
|
Object.assign(bannerPreview.style, {
|
|
backgroundColor: template.bgColor || '#f8f9fa',
|
|
color: template.textColor || '#212529',
|
|
textAlign: template.textAlign || 'left',
|
|
fontSize: `${template.fontSize || 16}px`,
|
|
padding: `${template.padding || 20}px`,
|
|
margin: `${template.margin || 20}px 0`,
|
|
borderRadius: `${template.borderRadius || 8}px`,
|
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
display: 'block',
|
|
overflow: 'hidden',
|
|
width: '100%',
|
|
maxWidth: '1200px',
|
|
transition: 'all 0.3s ease',
|
|
position: 'relative'
|
|
});
|
|
}
|
|
|
|
// Update the banner content with the generated HTML
|
|
if (bannerPreviewContent) {
|
|
bannerPreviewContent.innerHTML = bannerContent;
|
|
}
|
|
|
|
// Add event listener for visibility toggle
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const visibilityToggle = document.getElementById('bannerVisibility');
|
|
const hiddenInput = document.getElementById('bannerVisible');
|
|
|
|
if (visibilityToggle && hiddenInput) {
|
|
// Initialize visibility state
|
|
hiddenInput.value = visibilityToggle.checked ? 'true' : 'false';
|
|
|
|
// Add change event listener
|
|
visibilityToggle.addEventListener('change', function() {
|
|
hiddenInput.value = this.checked ? 'true' : 'false';
|
|
updateBannerPreview();
|
|
});
|
|
}
|
|
|
|
// Also update the banner preview when the page loads
|
|
updateBannerPreview();
|
|
});
|
|
|
|
// Add event listeners for width/height changes
|
|
const imageWidthInput = document.getElementById('bannerImageWidth');
|
|
const imageHeightInput = document.getElementById('bannerImageHeight');
|
|
|
|
if (imageWidthInput) {
|
|
imageWidthInput.value = template.imageWidth || 300;
|
|
imageWidthInput.addEventListener('input', updateBannerPreview);
|
|
}
|
|
|
|
if (imageHeightInput) {
|
|
imageHeightInput.value = template.imageHeight || 200;
|
|
imageHeightInput.addEventListener('input', updateBannerPreview);
|
|
}
|
|
|
|
// Make sure the preview is visible
|
|
if (bannerPreview) {
|
|
bannerPreview.style.visibility = 'visible';
|
|
}
|
|
|
|
// Update image position and visibility
|
|
const bannerImgElement = document.getElementById('bannerImagePreview');
|
|
const hasImageElement = bannerImgElement && !bannerImgElement.classList.contains('d-none') && bannerImgElement.src;
|
|
|
|
if (hasImageElement) {
|
|
const bannerLinkValue = document.getElementById('bannerLink')?.value || '';
|
|
|
|
if (bannerLinkValue) {
|
|
bannerImgElement.style.cursor = 'pointer';
|
|
bannerImgElement.onclick = (e) => {
|
|
e.preventDefault();
|
|
window.open(bannerLinkValue, '_blank');
|
|
};
|
|
} else {
|
|
bannerImgElement.style.cursor = 'default';
|
|
bannerImgElement.onclick = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply preset
|
|
function applyPreset(preset) {
|
|
const style = presets[preset];
|
|
if (!style) return;
|
|
|
|
bannerBgColor.value = style.backgroundColor;
|
|
bannerTextColor.value = style.textColor;
|
|
bannerTextAlign.value = style.textAlign;
|
|
|
|
// Update color pickers to match input fields
|
|
bannerBgColorPicker.value = style.backgroundColor;
|
|
bannerTextColorPicker.value = style.textColor;
|
|
|
|
updateColorPreviews();
|
|
updateBannerPreview();
|
|
}
|
|
|
|
// Event Listeners
|
|
// Debounced update for text inputs
|
|
const debouncedUpdatePreview = debounce(() => {
|
|
updateColorPreviews();
|
|
updateBannerPreview();
|
|
}, 300);
|
|
|
|
// This function will be called after DOM is loaded
|
|
function setupColorInputListeners() {
|
|
if (bannerBgColor) {
|
|
bannerBgColor.addEventListener('input', () => {
|
|
// Update color preview immediately
|
|
if (bgColorPreview) {
|
|
bgColorPreview.style.backgroundColor = bannerBgColor.value;
|
|
}
|
|
// Debounce the full preview update
|
|
debouncedUpdatePreview();
|
|
});
|
|
}
|
|
|
|
|
|
if (bannerTextColor) {
|
|
bannerTextColor.addEventListener('input', () => {
|
|
// Update color preview immediately
|
|
if (textColorPreview) {
|
|
textColorPreview.style.backgroundColor = bannerTextColor.value;
|
|
}
|
|
// Debounce the full preview update
|
|
debouncedUpdatePreview();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Connect color pickers to input fields
|
|
const bannerBgColorPicker = document.getElementById('bannerBgColorPicker');
|
|
const bannerTextColorPicker = document.getElementById('bannerTextColorPicker');
|
|
|
|
// Debounce function to improve performance
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function(...args) {
|
|
const context = this;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
|
};
|
|
}
|
|
|
|
// App management event listeners
|
|
document.getElementById('addAppBtn').addEventListener('click', openAddAppModal);
|
|
document.getElementById('closeAppModal').addEventListener('click', closeAppModal);
|
|
document.getElementById('cancelAppBtn').addEventListener('click', closeAppModal);
|
|
document.getElementById('appForm').addEventListener('submit', saveApp);
|
|
|
|
// Close modal when clicking outside
|
|
const appModal = document.getElementById('appModal');
|
|
appModal.addEventListener('click', (e) => {
|
|
if (e.target === appModal) {
|
|
closeAppModal();
|
|
}
|
|
});
|
|
|
|
// These event listeners will be set up after the DOM is fully loaded
|
|
|
|
// Setup draggable image functionality
|
|
function setupDraggableImage() {
|
|
const draggableImage = document.querySelector('.draggable-image');
|
|
if (!draggableImage) return;
|
|
|
|
// Remove any existing event listeners to prevent duplicates
|
|
const newDraggable = draggableImage.cloneNode(true);
|
|
draggableImage.parentNode.replaceChild(newDraggable, draggableImage);
|
|
|
|
let isDragging = false;
|
|
let startX, startY;
|
|
let originalX = parseInt(currentImageX) || 0;
|
|
let originalY = parseInt(currentImageY) || 0;
|
|
|
|
// Mouse events for desktop
|
|
newDraggable.addEventListener('mousedown', startDrag);
|
|
document.addEventListener('mousemove', drag);
|
|
document.addEventListener('mouseup', endDrag);
|
|
|
|
// Touch events for mobile
|
|
newDraggable.addEventListener('touchstart', startDragTouch);
|
|
document.addEventListener('touchmove', dragTouch);
|
|
document.addEventListener('touchend', endDrag);
|
|
|
|
function startDrag(e) {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
newDraggable.classList.add('dragging');
|
|
console.log('Started dragging at', startX, startY);
|
|
}
|
|
|
|
function startDragTouch(e) {
|
|
if (e.touches.length === 1) {
|
|
isDragging = true;
|
|
startX = e.touches[0].clientX;
|
|
startY = e.touches[0].clientY;
|
|
newDraggable.classList.add('dragging');
|
|
}
|
|
}
|
|
|
|
function drag(e) {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
const newX = originalX + deltaX;
|
|
const newY = originalY + deltaY;
|
|
|
|
newDraggable.style.left = `${newX}px`;
|
|
newDraggable.style.top = `${newY}px`;
|
|
|
|
// Update current position values
|
|
currentImageX = newX.toString();
|
|
currentImageY = newY.toString();
|
|
console.log('Dragging to', newX, newY);
|
|
}
|
|
|
|
function dragTouch(e) {
|
|
if (!isDragging || e.touches.length !== 1) return;
|
|
|
|
const deltaX = e.touches[0].clientX - startX;
|
|
const deltaY = e.touches[0].clientY - startY;
|
|
|
|
const newX = originalX + deltaX;
|
|
const newY = originalY + deltaY;
|
|
|
|
newDraggable.style.left = `${newX}px`;
|
|
newDraggable.style.top = `${newY}px`;
|
|
|
|
// Update current position values
|
|
currentImageX = newX.toString();
|
|
currentImageY = newY.toString();
|
|
}
|
|
|
|
function endDrag() {
|
|
if (!isDragging) return;
|
|
|
|
isDragging = false;
|
|
originalX = parseInt(currentImageX);
|
|
originalY = parseInt(currentImageY);
|
|
newDraggable.classList.remove('dragging');
|
|
console.log('Finished dragging at', originalX, originalY);
|
|
}
|
|
}
|
|
|
|
const templateConfigs = {
|
|
'default': {
|
|
name: 'Výchozí',
|
|
containerStyle: 'background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);',
|
|
textStyle: 'color: #2d3748; font-weight: 500;',
|
|
buttonStyle: 'background: #4a6cf7; color: white; border: none; padding: 8px 16px; border-radius: 4px;',
|
|
isVisible: true
|
|
},
|
|
'modern': {
|
|
name: 'Moderní',
|
|
containerStyle: 'background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);',
|
|
textStyle: 'color: #1a365d; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.1);',
|
|
buttonStyle: 'background: #2b6cb0; color: white; border: 2px solid #2c5282; padding: 8px 20px; border-radius: 25px;',
|
|
isVisible: true
|
|
},
|
|
'elegant': {
|
|
name: 'Elegantní',
|
|
containerStyle: 'background: linear-gradient(to right, #f5f7fa 0%, #e4e8f0 100%); border-left: 4px solid #4a5568;',
|
|
textStyle: 'color: #2d3748; font-family: Georgia, serif;',
|
|
buttonStyle: 'background: #4a5568; color: white; border: none; padding: 8px 16px; border-radius: 2px; letter-spacing: 1px;',
|
|
isVisible: true
|
|
},
|
|
'alert': {
|
|
name: 'Upozornění',
|
|
containerStyle: 'background: #fff3cd; border: 1px solid #ffeeba;',
|
|
textStyle: 'color: #856404;',
|
|
buttonStyle: 'background: #ffc107; color: #856404; border: 1px solid #d39e00; padding: 8px 16px; border-radius: 4px;',
|
|
isVisible: true
|
|
},
|
|
'dark': {
|
|
name: 'Tmavý motiv',
|
|
containerStyle: 'background: #2d3748;',
|
|
textStyle: 'color: #e2e8f0;',
|
|
buttonStyle: 'background: #4fd1c5; color: #1a202c; font-weight: 600; padding: 8px 16px; border-radius: 4px;',
|
|
isVisible: true
|
|
},
|
|
'gradient': {
|
|
name: 'Přechod',
|
|
containerStyle: 'background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);',
|
|
textStyle: 'color: white; text-shadow: 0 1px 3px rgba(0,0,0,0.2);',
|
|
buttonStyle: 'background: white; color: #4a6cf7; border: none; padding: 8px 20px; border-radius: 4px; font-weight: 600;',
|
|
isVisible: true
|
|
},
|
|
'gradient-blue': {
|
|
name: 'Modrý gradient',
|
|
containerStyle: 'background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: white;',
|
|
background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)',
|
|
backgroundColor: '#4f46e5',
|
|
textStyle: 'color: white; font-family: \'Inter\', sans-serif; font-size: 16px; font-weight: 500;',
|
|
buttonStyle: 'background-color: white; color: #4f46e5; border: none; padding: 10px 24px; border-radius: 6px; font-weight: 600;',
|
|
isVisible: true
|
|
}
|
|
};
|
|
|
|
// Setup template selection and generate template previews
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const templateGrid = document.getElementById('templateGrid');
|
|
|
|
// Get references to form elements
|
|
const bannerBgColorPicker = document.getElementById('bannerBgColorPicker');
|
|
const bannerBgColor = document.getElementById('bannerBgColor');
|
|
const bannerTextColorPicker = document.getElementById('bannerTextColorPicker');
|
|
const bannerTextColor = document.getElementById('bannerTextColor');
|
|
const bannerText = document.getElementById('bannerText');
|
|
const bannerLink = document.getElementById('bannerLink');
|
|
const bannerTextAlign = document.getElementById('bannerTextAlign');
|
|
const bannerFontSize = document.getElementById('bannerFontSize');
|
|
const bannerPadding = document.getElementById('bannerPadding');
|
|
const bannerMargin = document.getElementById('bannerMargin');
|
|
const bannerBorderRadius = document.getElementById('bannerBorderRadius');
|
|
const bannerVisibility = document.getElementById('bannerVisibility');
|
|
const stylePresets = document.querySelectorAll('.style-preset');
|
|
const saveBannerBtn = document.getElementById('saveBannerBtn');
|
|
|
|
// Set up color picker event listeners if elements exist
|
|
if (bannerBgColorPicker && bannerBgColor) {
|
|
bannerBgColorPicker.addEventListener('input', () => {
|
|
bannerBgColor.value = bannerBgColorPicker.value;
|
|
updateColorPreviews();
|
|
});
|
|
}
|
|
|
|
if (bannerTextColorPicker && bannerTextColor) {
|
|
bannerTextColorPicker.addEventListener('input', () => {
|
|
bannerTextColor.value = bannerTextColorPicker.value;
|
|
updateColorPreviews();
|
|
});
|
|
}
|
|
|
|
// For other form controls, use debounced updates on input
|
|
const formControls = [
|
|
bannerText, bannerLink, bannerTextAlign, bannerFontSize,
|
|
bannerPadding, bannerMargin, bannerBorderRadius, bannerVisibility
|
|
];
|
|
|
|
formControls.forEach(control => {
|
|
if (control) {
|
|
control.addEventListener('input', debounce(() => {
|
|
updateBannerPreview();
|
|
}, 300));
|
|
}
|
|
});
|
|
|
|
// Toggle visibility
|
|
if (bannerVisibility) {
|
|
bannerVisibility.addEventListener('change', () => {
|
|
updateBannerPreview();
|
|
});
|
|
}
|
|
|
|
// Style presets
|
|
if (stylePresets.length > 0) {
|
|
stylePresets.forEach(preset => {
|
|
preset.addEventListener('click', () => applyPreset(preset.dataset.preset));
|
|
});
|
|
}
|
|
|
|
// Save button
|
|
if (saveBannerBtn) {
|
|
saveBannerBtn.addEventListener('click', saveBanner);
|
|
}
|
|
|
|
// Generate template previews
|
|
if (templateGrid) {
|
|
// Clear existing content
|
|
templateGrid.innerHTML = '';
|
|
|
|
// Create a preview for each template
|
|
Object.entries(templateConfigs).forEach(([id, template]) => {
|
|
// Create template card container
|
|
const templateElement = document.createElement('div');
|
|
templateElement.className = 'group relative cursor-pointer rounded-lg overflow-hidden border border-gray-200 hover:border-blue-500 transition-all duration-200 hover:shadow-lg';
|
|
templateElement.dataset.templateId = id;
|
|
|
|
// Create preview container with aspect ratio 16:9
|
|
templateElement.innerHTML = `
|
|
<div class="relative pt-[56.25%] bg-white">
|
|
<div class="absolute inset-0 flex items-center justify-center p-4 text-center" style="${template.containerStyle}">
|
|
<div class="w-full h-full relative">
|
|
${template.imageStyle ?
|
|
`<div class="absolute inset-0" style="${template.imageStyle}">
|
|
<div class="w-full h-full bg-gray-200"></div>
|
|
</div>` :
|
|
''
|
|
}
|
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
<div class="text-center px-4 py-2 rounded-md bg-black bg-opacity-50 text-white" style="${template.textStyle}">
|
|
${template.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-white border-t border-gray-100">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-sm font-medium text-gray-900 truncate">${template.name}</span>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
Vybrat
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="absolute inset-0 bg-blue-500 bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-200"></div>
|
|
`;
|
|
|
|
// Add click event to apply template
|
|
templateElement.addEventListener('click', () => {
|
|
// Remove active class from all templates
|
|
document.querySelectorAll('[data-template-id]').forEach(el => {
|
|
el.classList.remove('ring-2', 'ring-blue-500');
|
|
});
|
|
// Add active class to selected template
|
|
templateElement.classList.add('ring-2', 'ring-blue-500');
|
|
applyTemplate(id);
|
|
});
|
|
|
|
// Add template to grid
|
|
templateGrid.appendChild(templateElement);
|
|
});
|
|
}
|
|
|
|
// Setup event listeners
|
|
function setupEventListeners() {
|
|
// Position switcher buttons
|
|
const positionButtons = document.querySelectorAll('.position-btn');
|
|
if (positionButtons.length > 0) {
|
|
// Set initial active state based on currentImagePosition
|
|
positionButtons.forEach(button => {
|
|
if (button.dataset.position === currentImagePosition) {
|
|
button.classList.add('active');
|
|
} else {
|
|
button.classList.remove('active');
|
|
}
|
|
|
|
button.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
// Remove active class from all buttons
|
|
positionButtons.forEach(btn => btn.classList.remove('active'));
|
|
// Add active class to clicked button
|
|
button.classList.add('active');
|
|
// Update current position
|
|
currentImagePosition = button.dataset.position;
|
|
console.log('Position changed to:', currentImagePosition);
|
|
// Update preview
|
|
updateBannerPreview();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Banner form submission
|
|
const bannerForm = document.getElementById('bannerForm');
|
|
if (bannerForm) {
|
|
bannerForm.addEventListener('submit', saveBanner);
|
|
}
|
|
|
|
// Image upload
|
|
const bannerImage = document.getElementById('bannerImage');
|
|
if (bannerImage) {
|
|
bannerImage.addEventListener('change', handleImageUpload);
|
|
}
|
|
|
|
// Remove image button
|
|
const removeImageBtn = document.getElementById('removeImageBtn');
|
|
if (removeImageBtn) {
|
|
removeImageBtn.addEventListener('click', removeImage);
|
|
}
|
|
|
|
// Template selection
|
|
const templateItems = document.querySelectorAll('.template-item');
|
|
templateItems.forEach(item => {
|
|
item.addEventListener('click', function() {
|
|
const templateId = this.dataset.template;
|
|
if (templateId) {
|
|
applyTemplate(templateId);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Image size controls
|
|
const sizeInputs = document.querySelectorAll('#bannerImageWidth, #bannerImageHeight');
|
|
sizeInputs.forEach(input => {
|
|
input.addEventListener('input', updateBannerPreview);
|
|
});
|
|
|
|
// Preview updates for text content
|
|
const previewInputs = [
|
|
document.getElementById('bannerText'),
|
|
document.getElementById('bannerLink'),
|
|
document.getElementById('bannerVisible')
|
|
];
|
|
|
|
previewInputs.forEach(input => {
|
|
if (input) {
|
|
input.addEventListener('input', updateBannerPreview);
|
|
input.addEventListener('change', updateBannerPreview);
|
|
}
|
|
});
|
|
|
|
// Checkbox for banner visibility
|
|
const bannerVisible = document.getElementById('bannerVisible');
|
|
if (bannerVisible) {
|
|
bannerVisible.addEventListener('change', updateBannerPreview);
|
|
}
|
|
|
|
// Initialize with default template
|
|
const defaultTemplate = document.querySelector(`[data-template="${currentTemplate}"]`);
|
|
if (defaultTemplate) {
|
|
defaultTemplate.classList.add('active');
|
|
}
|
|
|
|
// Initial preview update
|
|
updateBannerPreview();
|
|
|
|
// Show/hide templates and size controls based on image presence
|
|
const bannerTemplates = document.getElementById('bannerTemplates');
|
|
const imageSizeControls = document.getElementById('imageSizeControls');
|
|
const hasImage = currentImage || (bannerImage && bannerImage.files.length > 0);
|
|
|
|
if (bannerTemplates) {
|
|
bannerTemplates.style.display = hasImage ? 'block' : 'none';
|
|
}
|
|
if (imageSizeControls) {
|
|
imageSizeControls.style.display = hasImage ? 'flex' : 'none';
|
|
}
|
|
}
|
|
|
|
setupEventListeners();
|
|
|
|
// Show templates when image is uploaded
|
|
const bannerImage = document.getElementById('bannerImage');
|
|
if (bannerImage) {
|
|
bannerImage.addEventListener('change', () => {
|
|
const templates = document.getElementById('bannerTemplates');
|
|
if (templates) {
|
|
templates.style.display = 'block';
|
|
}
|
|
});
|
|
}
|
|
|
|
loadBanner();
|
|
});
|
|
|
|
// Apply selected template and update form fields
|
|
function applyTemplate(templateId) {
|
|
const template = templateConfigs[templateId];
|
|
if (!template) return;
|
|
|
|
// Store the selected template
|
|
currentTemplate = templateId;
|
|
|
|
// Update form fields with template styles
|
|
if (template.containerStyle) {
|
|
const bannerContainer = document.getElementById('bannerPreview');
|
|
if (bannerContainer) {
|
|
bannerContainer.style = template.containerStyle;
|
|
}
|
|
}
|
|
|
|
// Update text styles if they exist in the template
|
|
if (template.textStyle) {
|
|
const bannerText = document.getElementById('bannerText');
|
|
if (bannerText) {
|
|
bannerText.style = template.textStyle;
|
|
}
|
|
}
|
|
|
|
// Update button styles if they exist in the template
|
|
if (template.buttonStyle) {
|
|
const bannerButton = document.querySelector('.banner-button');
|
|
if (bannerButton) {
|
|
bannerButton.style = template.buttonStyle;
|
|
}
|
|
}
|
|
|
|
// Update the banner preview with the new template
|
|
updateBannerPreview();
|
|
|
|
// Show success message
|
|
showNotification(`Šablona "${template.name}" byla použita`, 'success');
|
|
|
|
// Scroll to preview to show the changes
|
|
const bannerPreview = document.getElementById('bannerPreview');
|
|
if (bannerPreview) {
|
|
bannerPreview.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |