Files
PPve/admin-dashboard.html
T
Tomas Dvorak f2cf831ad4 te
2025-06-11 22:25:13 +02:00

4607 lines
169 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);
}
/* Full Screen Icon Picker Modal */
#iconPickerModal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
padding: 2rem;
overflow: hidden;
}
#iconPickerModal::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
pointer-events: auto;
}
#iconPickerContainer {
max-width: 1200px;
margin: 2rem auto;
background: white;
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
overflow: hidden;
max-height: calc(100vh - 4rem);
display: flex;
flex-direction: column;
pointer-events: auto;
position: relative;
}
#iconListContainer {
overflow-y: auto;
max-height: calc(100vh - 200px); /* Adjust based on header height */
}
#iconPickerHeader {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
background: #f9fafb;
}
#iconPickerHeader h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #111827;
}
#closeIconPicker {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
padding: 0.5rem;
border-radius: 0.375rem;
transition: all 0.2s;
}
#closeIconPicker:hover {
background: #e5e7eb;
color: #111827;
}
#iconSearchContainer {
padding: 1.5rem;
background: white;
border-bottom: 1px solid #e5e7eb;
}
#iconSearch {
width: 100%;
padding: 1rem 1.25rem;
border: 2px solid #e5e7eb;
border-radius: 0.75rem;
font-size: 1.125rem;
transition: all 0.2s;
}
#iconSearch:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
#iconListContainer {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
background: white;
}
#iconSearch {
width: 100%;
padding: 0.75rem 1rem;
border: none;
border-bottom: 2px solid #e5e7eb;
border-radius: 0;
font-size: 0.9375rem;
margin: 0;
background-color: #f9fafb;
transition: all 0.2s ease;
}
#iconSearch:focus {
outline: none;
border-color: var(--primary-color);
background-color: white;
box-shadow: 0 2px 0 0 var(--primary-color);
}
#iconList {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 1rem;
}
#iconList::-webkit-scrollbar {
width: 8px;
height: 8px;
}
#iconList::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
#iconList::-webkit-scrollbar-thumb {
background-color: #cbd5e0;
border-radius: 4px;
}
.icon-category {
grid-column: 1 / -1;
margin: 0.5rem 0 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
background-color: #f9fafb;
border-radius: 0.25rem;
position: sticky;
top: 0;
z-index: 10;
}
.icon-option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem 0.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
background: white;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.icon-option i {
font-size: 2rem;
color: #4b5563;
margin-bottom: 0.5rem;
transition: all 0.2s ease;
pointer-events: none;
}
.icon-option .icon-name {
font-size: 0.75rem;
color: #6b7280;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
opacity: 1;
transform: translateY(0);
transition: all 0.2s ease;
pointer-events: none;
font-weight: 500;
}
.icon-option:hover {
background-color: #f8fafc;
border-color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.icon-option:hover i {
color: var(--primary-color);
transform: scale(1.15);
}
.icon-option:hover .icon-name {
opacity: 1;
transform: translateY(0);
}
.icon-option.active {
background-color: var(--primary-color);
border-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.3);
}
.icon-option.active i,
.icon-option.active .icon-name {
color: white;
}
/* Responsive adjustments */
@media (max-width: 1200px) {
#iconList {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
}
}
@media (max-width: 768px) {
#iconPickerContainer {
margin: 0.5rem;
max-height: calc(100vh - 1rem);
}
#iconList {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 0.75rem;
}
.icon-option i {
font-size: 1.75rem;
}
}
@media (max-width: 480px) {
#iconList {
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 0.5rem;
}
.icon-option {
padding: 1rem 0.25rem;
}
.icon-option i {
font-size: 1.5rem;
}
.icon-option .icon-name {
font-size: 0.6875rem;
}
}
.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;
}
.position-switcher {
display: flex;
gap: 10px;
margin: 15px 0;
background: #f8f9fa;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.position-btn {
flex: 1;
padding: 8px 12px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.position-btn i {
margin-right: 6px;
}
.position-btn:hover {
border-color: #4a6cf7;
color: #4a6cf7;
}
.position-btn.active {
background: #4a6cf7;
color: white;
border-color: #4a6cf7;
}
.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;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background-color: #f8f9fa;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.upload-box:hover {
border-color: #adb5bd;
background-color: #f1f3f5;
}
.upload-box.dragover,
.upload-box.border-primary {
border-color: #4a6cf7 !important;
background-color: #f0f4ff;
}
.upload-prompt {
pointer-events: none;
z-index: 1;
}
.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>
</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 z-50 overflow-y-auto p-4 hidden">
<div class="relative w-full max-w-4xl mx-auto p-4">
<div class="bg-white rounded-lg shadow-xl overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex justify-between items-center">
<h3 class="text-xl font-semibold text-gray-800" id="appModalTitle">Přidat aplikaci</h3>
<button id="closeAppModal" class="text-gray-400 hover:text-gray-500 focus:outline-none">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<form id="appForm" class="space-y-4 p-6">
<input type="hidden" id="appId">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Název aplikace</label>
<input type="text" id="name" name="name" class="form-control w-full" required>
</div>
<div class="form-group">
<label for="url" class="block text-sm font-medium text-gray-700 mb-1">Odkaz</label>
<input type="url" id="url" name="url" class="form-control w-full" required>
</div>
</div>
<div class="form-group">
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Popis</label>
<textarea id="description" name="description" rows="3" class="form-control w-full"></textarea>
</div>
</div>
<div class="space-y-4">
<div class="form-group">
<label class="block text-sm font-medium text-gray-700 mb-2">Ikona</label>
<div class="flex items-center space-x-4">
<input type="text" id="appIcon" name="iconClass" class="form-control w-full cursor-pointer" placeholder="Vyberte ikonu" readonly>
</div>
<div id="iconPreview" class="mt-2 flex items-center justify-center w-16 h-16 bg-gray-100 rounded-md overflow-hidden">
<i id="selectedIcon" class="fas fa-cube text-2xl text-gray-400"></i>
</div>
</div>
</div>
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200 mt-6">
<button type="button" id="cancelAppBtn" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Zrušit
</button>
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Uložit
</button>
</div>
</form>
</div>
</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 bg-white border-2 border-gray-300 rounded-b-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors"
contenteditable="true"
oninput="updateBannerPreview()"
style="min-height: 150px; padding: 15px; overflow-y: auto; line-height: 1.6; font-size: 16px; color: #1f2937; white-space: pre-wrap;">
Sem zadejte text banneru...
</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>
<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="dragDropArea" class="upload-box">
<input type="file" id="bannerImage" 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="uploadImageBtn">
<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>
</div>
</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>
<!-- Reservations Management Section -->
<div class="card" style="margin: 2rem auto; max-width: 1200px;">
<h3>Správa rezervací vozidel</h3>
<!-- Filters -->
<div class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label for="vehicleFilter" class="block text-sm font-medium text-gray-700 mb-1">Filtrovat dle vozidla:</label>
<select id="vehicleFilter" class="form-control w-full">
<option value="">Všechna vozidla</option>
<!-- Vehicle options will be populated by JavaScript -->
</select>
</div>
<div>
<label for="dateFilter" class="block text-sm font-medium text-gray-700 mb-1">Filtrovat dle data:</label>
<input type="date" id="dateFilter" class="form-control w-full">
</div>
<div class="flex items-end">
<button id="exportButton" class="btn btn-primary w-full">
<i class="fas fa-file-export mr-2"></i>Exportovat do Excelu
</button>
</div>
</div>
</div>
<!-- Reservations Table -->
<div class="overflow-x-auto">
<table id="reservationsTable" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Řidič</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vozidlo</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Od</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Do</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Účel</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Akce</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<!-- Rows will be populated by JavaScript -->
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
Načítám data o rezervacích...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Icon Picker Modal -->
<div id="iconPickerModal" class="fixed inset-0 z-50 hidden">
<div id="iconPickerContainer" class="bg-white rounded-xl shadow-2xl overflow-hidden flex flex-col">
<div id="iconPickerHeader" class="bg-white border-b border-gray-200 px-6 py-4">
<h3 class="text-xl font-semibold text-gray-900">Vyberte ikonu</h3>
<button id="closeIconPicker" class="text-gray-400 hover:text-gray-500">
<i class="fas fa-times"></i>
</button>
</div>
<div id="iconSearchContainer" class="px-6 py-4 border-b border-gray-200">
<input type="text" id="iconSearch" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Hledat ikony..." autocomplete="off">
</div>
<div id="iconListContainer" class="flex-1 overflow-y-auto">
<div id="iconList" class="p-6">
<!-- Icons will be populated by JavaScript -->
</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, '&emsp;');
}
});
});
// 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
}
// Banner variables will be initialized in DOMContentLoaded
let bannerBgColor, bannerTextColor, bannerText, bannerTextAlign, bannerFontSize, bannerPadding, bannerMargin, bannerBorderRadius, bannerPreview;
// Banner visibility state
let bannerVisible;
// 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
function setupBannerEventListeners() {
const bannerTextElement = document.getElementById('bannerText');
if (bannerTextElement) {
// Remove any existing event listeners to prevent duplicates
const newElement = bannerTextElement.cloneNode(true);
bannerTextElement.parentNode.replaceChild(newElement, bannerTextElement);
// Add new event listeners for input and blur
newElement.addEventListener('input', updateBannerPreview);
newElement.addEventListener('blur', updateBannerPreview);
// Also update when Enter is pressed (for multi-line text)
newElement.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
updateBannerPreview();
}
});
}
// Add event listeners for other preview inputs
const previewInputs = [
document.getElementById('bannerFontSize'),
document.getElementById('bannerTextColor'),
document.getElementById('bannerTextAlign'),
document.getElementById('bannerBgColor'),
document.getElementById('bannerLink'),
document.getElementById('bannerVisible')
];
previewInputs.forEach(input => {
if (input) {
input.addEventListener('input', updateBannerPreview);
input.addEventListener('change', updateBannerPreview);
}
});
}
// Initialize banner event listeners
setupBannerEventListeners();
// Get banner visibility element
const bannerVisible = document.getElementById('bannerVisible');
if (bannerVisible) {
bannerVisible.addEventListener('change', updateBannerPreview);
}
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
// 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() {
dropArea.classList.add('dragover');
}
function unhighlight() {
dropArea.classList.remove('dragover');
}
// Handle drop
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
window.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'
},
{
id: 'hardcoded-car-reservation',
name: 'Rezervace aut',
url: '/rezervace-aut.html',
description: 'Rezervace služebních vozů',
icon: 'fa-car',
color: 'blue'
}
];
console.log("HARDCODED_APPS defined:", window.HARDCODED_APPS);
// Load hardcoded apps
function loadHardcodedApps() {
console.log("Loading hardcoded apps...");
try {
const hardcodedAppsList = document.getElementById('hardcodedAppsList');
if (!hardcodedAppsList) {
console.error("hardcodedAppsList element not found");
return;
}
if (!window.HARDCODED_APPS || !Array.isArray(window.HARDCODED_APPS) || window.HARDCODED_APPS.length === 0) {
console.log("No hardcoded apps found");
hardcodedAppsList.innerHTML = `
<div class="text-center py-4 text-gray-500">
Žádné přednastavené aplikace nebyly nalezeny
</div>
`;
return;
}
console.log("Rendering", window.HARDCODED_APPS.length, "hardcoded apps");
hardcodedAppsList.innerHTML = window.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 || 'blue'}-100 text-${app.color || 'blue'}-600 flex items-center justify-center">
<i class="fas ${app.icon || 'fa-question'} 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>
${app.description ? `<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('');
} catch (error) {
console.error("Error in loadHardcodedApps:", error);
const hardcodedAppsList = document.getElementById('hardcodedAppsList');
if (hardcodedAppsList) {
hardcodedAppsList.innerHTML = `
<div class="text-center py-4 text-red-500">
Chyba při načítání přednastavených aplikací
</div>
`;
}
}
}
// 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);
// Filter out hardcoded apps and map only custom apps to HTML
const customApps = Array.isArray(apps)
? apps.filter(app => !app.id || !app.id.startsWith('hardcoded-'))
: [];
if (customApps.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;
}
console.log("Rendering", customApps.length, "dynamic apps");
dynamicAppsList.innerHTML = customApps.map(app => {
const iconToUse = app.iconClass || app.icon || 'fa-question';
return `
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between" data-app-id="${app.id}">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 rounded-full bg-${app.color || 'blue'}-100 text-${app.color || 'blue'}-600 flex items-center justify-center">
<i class="fas ${iconToUse} 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>
${app.description ? `<p class="text-sm text-gray-400">${app.description}</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('');
} 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() {
console.log("Starting to load all apps...");
try {
// First load hardcoded apps (synchronous)
console.log("Loading hardcoded apps...");
loadHardcodedApps();
// Then load dynamic apps (asynchronous)
console.log("Loading dynamic apps...");
await loadDynamicApps();
console.log("All apps loaded successfully");
} catch (error) {
console.error('Error loading apps:', error);
// Show error message in the UI
const appsList = document.getElementById('appsList');
if (appsList) {
const errorDiv = document.createElement('div');
errorDiv.className = 'bg-red-50 border-l-4 border-red-400 p-4 mb-4';
errorDiv.innerHTML = `
<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>
`;
appsList.insertBefore(errorDiv, appsList.firstChild);
}
}
}
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('appLink')?.value.trim() || '';
const description = document.getElementById('appDescription')?.value.trim() || '';
const iconClass = form.iconClass || 'fa-globe'; // Use stored icon class
const color = document.getElementById('appColor')?.value || '#4a6cf7';
console.log('Saving app with data:', { name, url, description, iconClass, color });
if (!name) {
showNotification('Název aplikace je povinný', 'error');
return;
}
if (!url) {
showNotification('URL adresa je povinná', 'error');
return;
}
// Prepare form data
formData.append('name', name);
formData.append('url', url);
formData.append('description', description);
formData.append('iconClass', iconClass); // Use iconClass instead of icon
formData.append('color', color);
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('appLink').value = app.url;
document.getElementById('appDescription').value = app.description || '';
document.getElementById('appModalTitle').textContent = 'Upravit aplikaci';
// Set color if exists
if (app.color) {
document.getElementById('appColor').value = app.color;
document.getElementById('appColorText').value = app.color;
}
// Handle icon preview
const iconPreview = document.getElementById('customIconPreview');
const selectedIcon = document.getElementById('selectedIcon');
const appIcon = document.getElementById('appIcon');
const customIconInput = document.getElementById('customIconInput');
// Reset all icon states first
if (iconPreview) {
iconPreview.src = '';
iconPreview.classList.add('hidden');
}
if (selectedIcon) {
selectedIcon.className = 'fas fa-cube text-2xl text-gray-400';
selectedIcon.classList.add('hidden');
}
// Set the appropriate icon based on the app data
const iconToUse = app.iconClass || app.icon; // Use iconClass if available, fallback to icon
if (iconToUse) {
if (iconToUse.startsWith('http') || iconToUse.startsWith('/') || iconToUse.startsWith('data:')) {
// Custom uploaded image
if (iconPreview) {
iconPreview.src = iconToUse;
iconPreview.classList.remove('hidden');
if (selectedIcon) selectedIcon.classList.add('hidden');
}
if (appIcon) appIcon.value = 'custom';
} else if (iconToUse.startsWith('fa-')) {
// Font Awesome icon
if (selectedIcon) {
selectedIcon.className = `fas ${iconToUse} text-2xl text-gray-400`;
selectedIcon.classList.remove('hidden');
if (iconPreview) iconPreview.classList.add('hidden');
}
if (appIcon) appIcon.value = iconToUse;
// Highlight the selected icon in the picker
const iconElements = document.querySelectorAll('.icon-option');
iconElements.forEach(el => {
if (el.getAttribute('data-icon') === app.icon) {
el.classList.add('selected');
} else {
el.classList.remove('selected');
}
});
}
} else {
// No icon
if (selectedIcon) {
selectedIcon.className = 'fas fa-cube text-2xl text-gray-400';
selectedIcon.classList.remove('hidden');
}
if (appIcon) appIcon.value = '';
}
// Reset file input
if (customIconInput) customIconInput.value = '';
// Show the modal
document.getElementById('appModal').classList.remove('hidden');
} catch (error) {
console.error('Error loading app:', error);
showNotification(error.message || 'Nastala chyba při načítání aplikace', '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 icon selection
const appIcon = document.getElementById('appIcon');
const customIconInput = document.getElementById('customIconInput');
const customIconPreview = document.getElementById('customIconPreview');
const selectedIcon = document.getElementById('selectedIcon');
if (appIcon) appIcon.value = '';
if (customIconInput) customIconInput.value = '';
if (customIconPreview) {
customIconPreview.src = '';
customIconPreview.classList.add('hidden');
}
if (selectedIcon) {
selectedIcon.className = 'fas fa-cube text-2xl text-gray-400';
selectedIcon.classList.remove('hidden');
}
// Reset color picker to default
const colorInput = document.getElementById('appColor');
const colorText = document.getElementById('appColorText');
if (colorInput) colorInput.value = '#4a6cf7';
if (colorText) colorText.value = '#4a6cf7';
// Reset icon picker selection
const selectedIcons = document.querySelectorAll('.icon-option.selected');
selectedIcons.forEach(icon => icon.classList.remove('selected'));
// Show the modal
const modal = document.getElementById('appModal');
if (modal) {
modal.classList.remove('hidden');
}
// Set focus to the first input field
const firstInput = form?.querySelector('input, textarea, select');
if (firstInput) firstInput.focus();
}
function closeAppModal() {
document.getElementById('appModal').classList.add('hidden');
}
// Handle file input change and preview
function setupFileInput() {
const fileInput = document.getElementById('customIconInput');
const previewImg = document.getElementById('customIconPreview');
const selectedIcon = document.getElementById('selectedIcon');
const appIcon = document.getElementById('appIcon');
if (!fileInput || !previewImg || !selectedIcon || !appIcon) return;
fileInput.addEventListener('change', function() {
const file = this.files[0];
if (!file) {
resetIconSelection();
return;
}
// Check file type
if (!file.type.startsWith('image/')) {
showNotification('Vyberte prosím obrázek (JPG, PNG, GIF, SVG)', 'warning');
resetIconSelection();
return;
}
// Check file size (max 2MB)
if (file.size > 2 * 1024 * 1024) {
showNotification('Obrázek je příliš velký. Maximální velikost je 2MB.', 'error');
resetIconSelection();
return;
}
// Create preview
const reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
previewImg.classList.remove('hidden');
selectedIcon.classList.add('hidden');
// Set a special value to indicate a custom icon is being used
appIcon.value = 'custom';
// Show success message
showNotification('Vlastní ikona byla úspěšně nahrána', 'success');
};
reader.onerror = function() {
showNotification('Chyba při načítání obrázku', 'error');
resetIconSelection();
};
reader.readAsDataURL(file);
});
function resetIconSelection() {
fileInput.value = '';
previewImg.src = '';
previewImg.classList.add('hidden');
selectedIcon.classList.remove('hidden');
appIcon.value = '';
}
// Color picker setup
const appColor = document.getElementById('appColor');
const appColorText = document.getElementById('appColorText');
if (appColor && appColorText) {
// Update text input when color picker changes
appColor.addEventListener('input', function() {
appColorText.value = this.value.toUpperCase();
});
// Update color picker when text input changes
appColorText.addEventListener('input', function() {
// Validate hex color
const colorRegex = /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i;
if (colorRegex.test(this.value)) {
// Ensure the # prefix is present
const hexColor = this.value.startsWith('#') ? this.value : `#${this.value}`;
appColor.value = hexColor;
}
});
}
};
// Reset form when modal is closed
// Initialize icon picker when the modal is shown
document.getElementById('appModal').addEventListener('show.bs.modal', function () {
// Initialize file input
setupFileInput();
// Initialize icon picker
initIconPicker();
// Set focus to search input when dropdown is shown
const iconInput = document.getElementById('appIcon');
if (iconInput) {
iconInput.addEventListener('focus', function() {
const dropdown = document.getElementById('iconDropdown');
if (dropdown) dropdown.classList.remove('hidden');
const search = document.getElementById('iconSearch');
if (search) search.focus();
});
}
});
// Handle modal hidden event
document.getElementById('appModal').addEventListener('hidden.bs.modal', function () {
const form = document.getElementById('appForm');
if (form) form.reset();
document.getElementById('appId').value = '';
document.getElementById('fileName').textContent = 'Výchozí ikona';
document.getElementById('appIconClass').value = 'fa-globe';
document.getElementById('selectedIcon').className = 'fas fa-globe text-xl';
document.getElementById('iconPreview').className = 'w-12 h-12 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center mr-3';
// Reset file input
const fileInput = document.getElementById('appIcon');
if (fileInput) {
fileInput.value = '';
}
// Hide dropdown if it's still visible
const dropdown = document.getElementById('iconDropdown');
if (dropdown) dropdown.classList.add('hidden');
});
// Icon picker functionality with more categories and icons
const iconCategories = {
'Doprava': ['car', 'car-side', 'truck', 'bus', 'bicycle', 'motorcycle', 'plane', 'plane-departure', 'ship', 'subway', 'train', 'train-subway', 'walking', 'gas-pump', 'map-marker-alt', 'route'],
'Jídlo a nápoje': ['utensils', 'hamburger', 'pizza-slice', 'ice-cream', 'coffee', 'mug-hot', 'beer', 'wine-glass', 'wine-bottle', 'wine-glass-alt', 'wine-bottle-alt', 'apple-alt', 'bread-slice', 'cheese', 'drumstick-bite', 'egg', 'fish', 'hotdog', 'ice-cream', 'lemon', 'pepper-hot', 'shrimp', 'stroopwafel'],
'Nástroje': ['tools', 'wrench', 'screwdriver', 'hammer', 'toolbox', 'ruler', 'ruler-combined', 'ruler-horizontal', 'ruler-vertical', 'screwdriver-wrench', 'screwdriver', 'hammer', 'paint-roller', 'paint-brush', 'pencil-ruler', 'ruler', 'screwdriver', 'toolbox', 'wrench'],
'Kancelář': ['briefcase', 'folder', 'folder-open', 'file', 'file-alt', 'file-archive', 'file-audio', 'file-code', 'file-excel', 'file-image', 'file-pdf', 'file-word', 'file-powerpoint', 'file-signature', 'file-upload', 'file-download', 'file-export', 'file-import', 'file-invoice', 'file-invoice-dollar', 'file-medical', 'file-prescription'],
'Lidé': ['user', 'user-alt', 'user-astronaut', 'user-check', 'user-circle', 'user-clock', 'user-cog', 'user-edit', 'user-friends', 'user-graduate', 'user-injured', 'user-lock', 'user-md', 'user-ninja', 'user-nurse', 'user-plus', 'user-secret', 'user-shield', 'user-tag', 'user-tie', 'users', 'users-cog', 'user-tie'],
'Komunikace': ['envelope', 'envelope-open', 'envelope-open-text', 'envelope-square', 'inbox', 'comment', 'comments', 'comment-alt', 'comment-dots', 'comment-medical', 'comment-slash', 'comment-alt', 'comments', 'inbox', 'mail-bulk', 'phone', 'phone-alt', 'phone-slash', 'phone-square', 'phone-square-alt', 'phone-volume', 'sms', 'voicemail'],
'Sociální sítě': ['thumbs-up', 'thumbs-down', 'share', 'share-alt', 'share-square', 'retweet', 'reply', 'comment', 'comments', 'heart', 'heart-broken', 'star', 'star-half-alt', 'thumbs-up', 'thumbs-down', 'user-plus', 'user-friends', 'user-check', 'user-tag', 'user-shield'],
'Finance': ['money-bill', 'money-bill-wave', 'money-bill-alt', 'money-check', 'money-check-alt', 'credit-card', 'credit-card-alt', 'wallet', 'donate', 'dollar-sign', 'euro-sign', 'lira-sign', 'pound-sign', 'rupee-sign', 'shekel-sign', 'yen-sign', 'bitcoin', 'ethereum', 'btc', 'euro', 'gg', 'gg-circle', 'ils', 'krw', 'money-bill', 'money-bill-alt', 'money-bill-wave', 'money-bill-wave-alt', 'money-check', 'money-check-alt', 'receipt', 'ruble-sign', 'rupee-sign', 'shekel-sign', 'tenge', 'won-sign', 'yen-sign'],
'Zdraví': ['heart', 'heartbeat', 'heart-broken', 'hospital', 'hospital-alt', 'hospital-symbol', 'ambulance', 'band-aid', 'briefcase-medical', 'capsules', 'clinic-medical', 'diagnoses', 'disease', 'dna', 'file-medical', 'file-medical-alt', 'file-prescription', 'first-aid', 'heart', 'heartbeat', 'hospital', 'hospital-alt', 'hospital-symbol', 'hospital-user', 'id-card', 'id-card-alt', 'notes-medical', 'pills', 'plus', 'plus-circle', 'plus-square', 'prescription', 'prescription-bottle', 'prescription-bottle-alt', 'procedures', 'sign-in-alt', 'sign-out-alt', 'stethoscope', 'syringe', 'tablets', 'teeth', 'teeth-open', 'thermometer', 'user-md', 'user-nurse', 'vial', 'vials', 'weight', 'weight-hanging', 'wheelchair'],
'Vzdělání': ['graduation-cap', 'university', 'school', 'chalkboard', 'chalkboard-teacher', 'book', 'book-open', 'book-reader', 'bookmark', 'brain', 'calculator', 'chalkboard', 'chalkboard-teacher', 'graduation-cap', 'school', 'university', 'user-graduate', 'user-tie']
};
// Initialize icon picker with simplified modal
function initIconPicker() {
const iconInput = document.getElementById('appIcon');
const iconPickerModal = document.getElementById('iconPickerModal');
const closeButton = document.getElementById('closeIconPicker');
const iconSearch = document.getElementById('iconSearch');
const iconList = document.getElementById('iconList');
if (!iconInput || !iconPickerModal) return;
let isModalOpen = false;
// Simple show/hide functions
const toggleModal = () => {
isModalOpen = !isModalOpen;
document.body.style.overflow = isModalOpen ? 'hidden' : '';
iconPickerModal.style.display = isModalOpen ? 'block' : 'none';
if (isModalOpen) {
// Focus search input only after modal is visible
requestAnimationFrame(() => {
if (iconSearch) {
iconSearch.focus();
}
});
}
};
// Toggle modal on icon input click
iconInput.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
toggleModal();
});
// Close modal handlers
if (closeButton) {
closeButton.addEventListener('click', () => {
isModalOpen = false;
document.body.style.overflow = '';
iconPickerModal.style.display = 'none';
});
}
// Close when clicking outside the modal content
iconPickerModal.addEventListener('click', (e) => {
if (e.target === iconPickerModal) {
isModalOpen = false;
document.body.style.overflow = '';
iconPickerModal.style.display = 'none';
}
});
// Handle icon selection
if (iconList) {
iconList.addEventListener('click', (e) => {
const iconOption = e.target.closest('.icon-option');
if (iconOption) {
const iconClass = iconOption.getAttribute('data-icon');
selectIcon(iconClass);
}
});
}
// Handle search
if (iconSearch) {
iconSearch.addEventListener('input', () => {
// Debounce search to prevent excessive re-renders
clearTimeout(iconSearch.dataset.searchTimeout);
iconSearch.dataset.searchTimeout = setTimeout(() => {
renderIcons(iconSearch.value.toLowerCase());
}, 200);
});
}
// Initial render
renderIcons('');
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isModalOpen) {
isModalOpen = false;
document.body.style.overflow = '';
iconPickerModal.style.display = 'none';
}
});
}
// Render icons based on search term
function renderIcons(searchTerm) {
const iconList = document.getElementById('iconList');
if (!iconList) return;
let iconsHtml = '';
let hasVisibleIcons = false;
// Add icons from all categories
for (const [category, icons] of Object.entries(iconCategories)) {
const filteredIcons = icons.filter(icon =>
icon.toLowerCase().includes(searchTerm) ||
category.toLowerCase().includes(searchTerm)
);
if (filteredIcons.length > 0) {
hasVisibleIcons = true;
iconsHtml += `<div class="icon-category">${category}</div>`;
filteredIcons.forEach(icon => {
const iconClass = `fa-${icon}`;
const displayName = icon.replace(/-/g, ' ');
iconsHtml += `
<div class="icon-option"
data-icon="${iconClass}"
title="${displayName}">
<i class="fas ${iconClass}"></i>
<span class="icon-name">${displayName}</span>
</div>`;
});
}
}
if (!hasVisibleIcons) {
iconsHtml = `
<div class="col-span-full text-center py-8 text-gray-500">
<i class="fas fa-search mb-2 text-2xl"></i>
<p>Žádné ikony nenalezeny</p>
</div>`;
}
iconList.innerHTML = iconsHtml;
// Add click handlers to icon options
document.querySelectorAll('.icon-option').forEach(option => {
option.addEventListener('click', function() {
const iconClass = this.getAttribute('data-icon');
selectIcon(iconClass);
});
});
}
// Filter icons based on search input (now handled in renderIcons)
// Select an icon
function selectIcon(iconClass) {
const iconPickerModal = document.getElementById('iconPickerModal');
const selectedIcon = document.getElementById('selectedIcon');
const iconPreview = document.getElementById('iconPreview');
const appIcon = document.getElementById('appIcon');
const iconClassInput = document.getElementById('appIconClass');
// Show the selected icon
if (selectedIcon) {
selectedIcon.className = `fas ${iconClass} text-2xl text-gray-400`;
selectedIcon.classList.remove('hidden');
}
// Set the app icon value to the selected icon class
if (appIcon) appIcon.value = iconClass;
if (iconClassInput) iconClassInput.value = iconClass;
// Update preview with random color
const colors = ['blue', 'green', 'red', 'yellow', 'indigo', 'purple', 'pink', 'gray'];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
if (iconPreview) {
iconPreview.className = `mt-2 flex items-center justify-center w-16 h-16 bg-${randomColor}-100 rounded-md overflow-hidden`;
}
// Close the modal
if (iconPickerModal) {
iconPickerModal.classList.add('hidden');
isModalOpen = false;
document.body.style.overflow = '';
}
// Remove active class from all icons
document.querySelectorAll('.icon-option').forEach(option => {
option.classList.remove('active');
});
// Add active class to selected icon
const selectedOption = document.querySelector(`.icon-option[data-icon="${iconClass}"]`);
if (selectedOption) {
selectedOption.classList.add('active');
}
// Store the icon class in the form data
const form = document.getElementById('appForm');
if (form) {
form.iconClass = iconClass;
}
}
// Initialize icon picker when the page loads
document.addEventListener('DOMContentLoaded', function() {
// Initialize banner visibility
bannerVisible = document.getElementById('bannerVisibility');
// Initialize icon picker
initIconPicker();
// Toggle icon dropdown when clicking the icon input
const iconInput = document.getElementById('appIcon');
const iconDropdown = document.getElementById('iconDropdown');
if (iconInput && iconDropdown) {
iconInput.addEventListener('focus', function() {
iconDropdown.classList.remove('hidden');
});
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
if (!iconInput.contains(event.target) && !iconDropdown.contains(event.target)) {
iconDropdown.classList.add('hidden');
}
});
}
// Update icon preview when editing an existing app
const appModal = document.getElementById('appModal');
if (appModal) {
appModal.addEventListener('shown.bs.modal', function() {
const iconClass = document.getElementById('appIconClass')?.value;
if (iconClass) {
selectIcon(iconClass);
}
});
}
});
// Initialize file input handling when the page loads
document.addEventListener('DOMContentLoaded', () => {
setupFileInput();
});
// Delete app function
async function deleteApp(appId) {
if (!confirm('Opravdu chcete smazat tuto aplikaci? Tuto akci nelze vrátit zpět.')) {
return;
}
try {
const token = localStorage.getItem('token');
if (!token) {
window.location.href = '/login.html';
return;
}
const response = await fetch(`/api/apps/${appId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Nepodařilo se smazat aplikaci');
}
// Reload the apps list
await loadDynamicApps();
showNotification('Aplikace byla úspěšně smazána', 'success');
} catch (error) {
console.error('Error deleting app:', error);
showNotification(error.message || 'Nastala chyba při mazání aplikace', 'error');
}
}
// Save app function
async function saveApp(event) {
event.preventDefault();
// Get form values
const name = document.getElementById('name').value.trim();
const url = document.getElementById('url').value.trim();
const description = document.getElementById('description').value.trim();
const iconClass = document.getElementById('appIcon').value.trim();
const appId = document.getElementById('appId').value;
// Validate required fields
if (!name || !url || !iconClass) {
showNotification('Název, URL a ikona jsou povinné pole', 'error');
return;
}
// Create form data
const formData = new URLSearchParams();
formData.append('name', name);
formData.append('url', url);
formData.append('description', description);
formData.append('iconClass', iconClass);
// Create request URL
const requestUrl = appId ? `/api/apps/${appId}` : '/api/apps';
const method = appId ? 'PUT' : 'POST';
try {
const response = await fetch(requestUrl, {
method: method,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
credentials: 'include'
});
if (!response.ok) {
const errorText = await response.text();
console.error('Server response:', errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
showNotification('Aplikace byla úspěšně uložena', 'success');
loadApps();
closeAppModal();
} catch (error) {
console.error('Error saving app:', error);
showNotification(`Chyba při ukládání aplikace: ${error.message}`, 'error');
}
}
// Logout functionality
document.getElementById('logoutBtn').addEventListener('click', function() {
localStorage.removeItem('token');
window.location.href = '/';
});
// DOM Elements - these will be initialized in DOMContentLoaded
let 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 => {
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');
// Get current values from form or use template defaults
const bgColor = bannerBgColorPicker?.value || (currentTemplate && templateConfigs[currentTemplate]?.backgroundColor) || '#f8f9fa';
const textColor = bannerTextColorPicker?.value || (currentTemplate && templateConfigs[currentTemplate]?.textColor) || '#212529';
const textAlign = bannerTextAlign?.value || (currentTemplate && templateConfigs[currentTemplate]?.textAlign) || 'left';
const fontSize = bannerFontSize?.value || (currentTemplate && templateConfigs[currentTemplate]?.fontSize) || '16';
const padding = bannerPadding?.value || (currentTemplate && templateConfigs[currentTemplate]?.padding) || '20';
const margin = bannerMargin?.value || (currentTemplate && templateConfigs[currentTemplate]?.margin) || '20';
const borderRadius = bannerBorderRadius?.value || (currentTemplate && templateConfigs[currentTemplate]?.borderRadius) || '4';
const buttonBg = (currentTemplate && templateConfigs[currentTemplate]?.buttonBackground) || '#4a6cf7';
const buttonTextColor = (currentTemplate && templateConfigs[currentTemplate]?.buttonTextColor) || '#ffffff';
const buttonBorder = (currentTemplate && templateConfigs[currentTemplate]?.buttonBorder) || 'none';
const background = (currentTemplate && templateConfigs[currentTemplate]?.background) || '';
// Add style values with proper field names
formData.append('Style[BackgroundColor]', bgColor);
formData.append('Style[TextColor]', textColor);
formData.append('Style[TextAlign]', textAlign);
formData.append('Style[FontSize]', fontSize);
formData.append('Style[Padding]', padding);
formData.append('Style[Margin]', margin);
formData.append('Style[BorderRadius]', borderRadius);
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');
// Add button styles
formData.append('Style[ButtonBackground]', buttonBg);
formData.append('Style[ButtonTextColor]', buttonTextColor);
formData.append('Style[ButtonBorder]', buttonBorder);
// Add background style if defined in template
if (background) {
formData.append('Style[Background]', background);
}
// 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 bannerTextElement = document.getElementById('bannerText');
const bannerText = bannerTextElement ? bannerTextElement.innerText || bannerTextElement.textContent : '';
const bannerVisible = document.getElementById('bannerVisible')?.checked !== false;
const bannerTemplates = document.getElementById('bannerTemplates');
const imagePreview = document.getElementById('imagePreview');
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
// Get the current template config or use default if none selected
const defaultTemplate = templateConfigs['default'] || {};
const template = currentTemplate ? (templateConfigs[currentTemplate] || defaultTemplate) : defaultTemplate;
const fileInput = document.getElementById('bannerImage');
const hasImage = Boolean(currentImage || (fileInput && fileInput.files && fileInput.files.length > 0));
// Get all form field values or use template defaults
const bannerBgColor = document.getElementById('bannerBgColor')?.value || template.backgroundColor || '#f8f9fa';
const bannerTextColor = document.getElementById('bannerTextColor')?.value || template.textColor || '#212529';
const bannerTextAlign = document.getElementById('bannerTextAlign')?.value || template.textAlign || 'left';
// Debug log for text color
console.log('Text color values:', {
formField: document.getElementById('bannerTextColor')?.value,
template: template.textColor,
final: bannerTextColor
});
const bannerFontSize = document.getElementById('bannerFontSize')?.value || template.fontSize || 16;
const bannerPadding = document.getElementById('bannerPadding')?.value || template.padding || 20;
const bannerMargin = document.getElementById('bannerMargin')?.value || template.margin || 20;
const bannerBorderRadius = document.getElementById('bannerBorderRadius')?.value || template.borderRadius || 8;
const bannerButtonBackground = document.getElementById('bannerButtonBackground')?.value || template.buttonBackground || '#4a6cf7';
const bannerButtonTextColor = document.getElementById('bannerButtonTextColor')?.value || template.buttonTextColor || '#ffffff';
const bannerButtonBorder = document.getElementById('bannerButtonBorder')?.value || template.buttonBorder || 'none';
// Always show templates section
if (bannerTemplates) {
bannerTemplates.style.display = 'block';
}
// Create banner content based on template
let bannerContent = '';
const bannerLink = document.getElementById('bannerLink')?.value || '';
const bannerTextContent = bannerText || 'Náhled banneru';
// Get all styles from form fields or template defaults
const styles = {
// Background styles
background: template.background || bannerBgColor,
backgroundColor: bannerBgColor,
// Text styles
color: bannerTextColor,
textColor: bannerTextColor,
textAlign: bannerTextAlign,
fontSize: `${bannerFontSize}px`,
// Layout styles
padding: `${bannerPadding}px`,
margin: `${bannerMargin}px`,
borderRadius: `${bannerBorderRadius}px`,
// Button styles (if template defines them)
buttonBackground: bannerButtonBackground,
buttonTextColor: bannerButtonTextColor,
buttonBorder: bannerButtonBorder
};
// Create the banner content with proper text color inheritance
const textElement = `
<div class="banner-text" style="
font-size: ${styles.fontSize};
color: inherit !important;
text-align: ${styles.textAlign};
margin: 0;
padding: 20px 0;
line-height: 1.5;
flex: 1;
${template.textStyle || ''}
${styles.textShadow ? `text-shadow: ${styles.textShadow};` : ''}
">
${bannerTextContent}
</div>`;
// Handle image if present
let imgContainer = '';
if (hasImage && currentImage) {
imgContainer = `
<div class="banner-image-container" style="
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
align-items: center;
margin-left: 20px;
order: 1;
">
<img
src="${currentImage}"
style="
max-width: 300px;
max-height: 200px;
width: auto;
height: auto;
object-fit: contain;
border-radius: 8px;
display: block;
"
alt="Nahraný obrázek"
class="banner-image"
draggable="false"
>
</div>`;
// Wrap image with link if URL is provided
if (bannerLink) {
imgContainer = `
<a href="${bannerLink}" target="_blank" style="text-decoration: none;">
${imgContainer}
</a>`;
}
}
// Build the background style
let backgroundStyle = styles.background && styles.background.includes('gradient')
? `background: ${styles.background}`
: `background-color: ${styles.backgroundColor || '#f8f9fa'}`;
// Create the banner container with proper flex layout
bannerContent = `
<div class="banner" style="
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 200px;
margin: ${styles.margin};
padding: ${styles.padding};
${backgroundStyle};
color: ${styles.textColor} !important;
border-radius: ${styles.borderRadius};
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-size: ${styles.fontSize};
text-align: ${styles.textAlign};
transition: all 0.3s ease;
position: relative;
overflow: hidden;
--banner-text-color: ${styles.textColor};
">
<div class="banner-content" style="
flex: 1;
padding: 0 20px;
color: var(--banner-text-color, ${styles.textColor}) !important;
text-align: ${styles.textAlign};
font-size: ${styles.fontSize};
">
<div style="color: inherit !important; text-align: inherit;">
${bannerTextContent}
</div>
${template.buttonText ? `
<div style="margin-top: 15px;">
<a href="${bannerLink || '#'}"
target="_blank"
class="banner-button"
style="
display: inline-block;
padding: 8px 20px;
background-color: ${styles.buttonBackground};
color: ${styles.buttonTextColor};
text-decoration: none;
border-radius: 4px;
border: ${styles.buttonBorder};
font-size: ${parseInt(styles.fontSize) - 2}px;
transition: all 0.3s ease;
"
onmouseover="this.style.opacity='0.9'; this.style.transform='translateY(-2px)';"
onmouseout="this.style.opacity='1'; this.style.transform='translateY(0)';">
${template.buttonText}
</a>
</div>` : ''}
</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';
}
// Add the with-image class to the banner preview for proper spacing
if (bannerPreview) {
bannerPreview.classList.add('with-image');
}
} catch (error) {
console.error('Error updating banner preview:', error);
}
// Apply template styles to the banner preview container
if (template && bannerPreview) {
Object.assign(bannerPreview.style, {
backgroundColor: 'transparent',
padding: '0',
margin: '20px 0',
boxShadow: 'none',
display: 'block',
width: '100%',
maxWidth: '1200px',
transition: 'all 0.3s ease',
position: 'relative',
overflow: 'visible'
});
}
// 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í',
background: 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)',
backgroundColor: '#f5f7fa',
textColor: '#2d3748',
textStyle: 'color: #2d3748; font-weight: 500;',
buttonStyle: 'background: #4a6cf7; color: white; border: none; padding: 8px 16px; border-radius: 4px;',
isVisible: true,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 4,
buttonBackground: '#4a6cf7',
buttonTextColor: '#ffffff',
buttonBorder: 'none'
},
'modern': {
name: 'Moderní',
background: 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)',
backgroundColor: '#84fab0',
textColor: '#1a365d',
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,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 25,
buttonBackground: '#2b6cb0',
buttonTextColor: '#ffffff',
buttonBorder: '2px solid #2c5282'
},
'elegant': {
name: 'Elegantní',
background: 'linear-gradient(to right, #f5f7fa 0%, #e4e8f0 100%)',
backgroundColor: '#f5f7fa',
textColor: '#2d3748',
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,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 8,
buttonBackground: '#4a5568',
buttonTextColor: '#ffffff',
buttonBorder: 'none'
},
'alert': {
name: 'Upozornění',
background: '#fff3cd',
backgroundColor: '#fff3cd',
textColor: '#856404',
textStyle: 'color: #856404;',
buttonStyle: 'background: #ffc107; color: #856404; border: 1px solid #d39e00; padding: 8px 16px; border-radius: 4px;',
isVisible: true,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 4,
buttonBackground: '#ffc107',
buttonTextColor: '#856404',
buttonBorder: '1px solid #d39e00'
},
'dark': {
name: 'Tmavý motiv',
background: '#2d3748',
backgroundColor: '#2d3748',
textColor: '#e2e8f0',
textStyle: 'color: #e2e8f0;',
buttonStyle: 'background: #4fd1c5; color: #1a202c; font-weight: 600; padding: 8px 16px; border-radius: 4px;',
isVisible: true,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 4,
buttonBackground: '#4fd1c5',
buttonTextColor: '#1a202c',
buttonBorder: 'none'
},
'gradient': {
name: 'Přechod',
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
backgroundColor: '#667eea',
textColor: 'white',
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,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 4,
buttonBackground: 'white',
buttonTextColor: '#4a6cf7',
buttonBorder: 'none'
},
'gradient-blue': {
name: 'Modrý gradient',
background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)',
backgroundColor: '#4f46e5',
textColor: 'white',
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,
textAlign: 'left',
fontSize: 16,
padding: 20,
margin: 20,
borderRadius: 6,
buttonBackground: 'white',
buttonTextColor: '#4f46e5',
buttonBorder: 'none'
}
};
// 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="background: ${template.background || template.backgroundColor || '#ffffff'}; color: ${template.textColor || '#000000'}; text-align: ${template.textAlign || 'center'}; padding: ${template.padding || 20}px; border-radius: ${template.borderRadius || 0}px;">
<div class="w-full h-full relative">
<div class="absolute inset-0 flex items-center justify-center p-4">
<div class="text-center px-4 py-2 rounded-md" style="background: ${template.buttonBackground || '#4a6cf7'}; color: ${template.buttonTextColor || '#ffffff'}; border: ${template.buttonBorder || 'none'}; padding: 8px 16px; border-radius: 4px; font-weight: 500;">
${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;
// Update the hidden input value
const positionInput = document.getElementById('bannerImagePosition');
if (positionInput) {
positionInput.value = currentImagePosition;
}
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 is already handled by the bannerImage change event in the main setup
// 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);
}
});
// Show/hide templates and size controls based on image presence
const bannerTemplates = document.getElementById('bannerTemplates');
const imageSizeControls = document.getElementById('imageSizeControls');
const bannerImage = document.getElementById('bannerImage');
const hasImage = currentImage || (bannerImage && bannerImage.files && bannerImage.files.length > 0);
if (bannerTemplates) {
bannerTemplates.style.display = hasImage ? 'block' : 'none';
}
if (imageSizeControls) {
imageSizeControls.style.display = hasImage ? 'flex' : 'none';
}
// Show templates when image is uploaded
if (bannerImage) {
bannerImage.addEventListener('change', () => {
const templates = document.getElementById('bannerTemplates');
if (templates) {
templates.style.display = 'block';
}
});
}
}; // Close setupEventListeners function
// Initialize event listeners
setupEventListeners();
// Load banner data
loadBanner();
});
// Apply selected template and update form fields
function applyTemplate(templateId) {
const template = templateConfigs[templateId];
if (!template) return;
console.log('Applying template:', templateId, template);
// Store the selected template
currentTemplate = templateId;
// Update form fields with template styles
const bannerBgColor = document.getElementById('bannerBgColor');
const bannerTextColor = document.getElementById('bannerTextColor');
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 bannerButtonBackground = document.getElementById('bannerButtonBackground');
const bannerButtonTextColor = document.getElementById('bannerButtonTextColor');
const bannerButtonBorder = document.getElementById('bannerButtonBorder');
// Update background color (use background if available, otherwise use backgroundColor)
if (bannerBgColor) {
const bgColor = template.background || template.backgroundColor || '#ffffff';
bannerBgColor.value = bgColor;
// Update color picker if it exists
const bannerBgColorPicker = document.getElementById('bannerBgColorPicker');
if (bannerBgColorPicker) bannerBgColorPicker.value = bgColor;
}
// Update text color
if (bannerTextColor && template.textColor) {
bannerTextColor.value = template.textColor;
// Update color picker if it exists
const bannerTextColorPicker = document.getElementById('bannerTextColorPicker');
if (bannerTextColorPicker) bannerTextColorPicker.value = template.textColor;
}
// Update text alignment
if (bannerTextAlign && template.textAlign) {
bannerTextAlign.value = template.textAlign;
}
// Update font size
if (bannerFontSize && template.fontSize) {
bannerFontSize.value = template.fontSize;
}
// Update padding
if (bannerPadding && template.padding) {
bannerPadding.value = template.padding;
}
// Update margin
if (bannerMargin && template.margin) {
bannerMargin.value = template.margin;
}
// Update border radius
if (bannerBorderRadius && template.borderRadius) {
bannerBorderRadius.value = template.borderRadius;
}
// Update button styles
if (bannerButtonBackground && template.buttonBackground) {
bannerButtonBackground.value = template.buttonBackground;
}
if (bannerButtonTextColor && template.buttonTextColor) {
bannerButtonTextColor.value = template.buttonTextColor;
}
if (bannerButtonBorder && template.buttonBorder) {
bannerButtonBorder.value = template.buttonBorder;
}
// Update color previews
updateColorPreviews();
// 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' });
}
}
// Load apps when the page loads
document.addEventListener('DOMContentLoaded', function() {
loadApps();
// Initialize banner image upload functionality
const dragDropArea = document.getElementById('dragDropArea');
const uploadImageBtn = document.getElementById('uploadImageBtn');
const bannerImageInput = document.getElementById('bannerImage');
if (dragDropArea && uploadImageBtn && bannerImageInput) {
console.log('Initializing banner upload functionality...');
// Handle file selection via button
uploadImageBtn.addEventListener('click', (e) => {
console.log('Upload button clicked');
e.preventDefault(); // Prevent form submission
e.stopPropagation(); // Stop event bubbling
bannerImageInput.click();
}, false);
// Make the entire drop zone clickable
dragDropArea.addEventListener('click', (e) => {
// Only trigger file input if clicking directly on the drop zone, not on the button
if (e.target === dragDropArea) {
console.log('Drop zone clicked');
bannerImageInput.click();
}
});
// Handle file input change
bannerImageInput.addEventListener('change', (e) => {
console.log('File input changed');
if (e.target.files && e.target.files[0]) {
console.log('File selected:', e.target.files[0].name);
handleFileSelect(e.target.files[0]);
}
}, false);
// Handle drag and drop
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dragDropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dragDropArea.addEventListener(eventName, () => {
console.log('Drag over drop zone');
dragDropArea.classList.add('border-primary', 'bg-blue-50');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dragDropArea.addEventListener(eventName, () => {
console.log('Drag left drop zone');
dragDropArea.classList.remove('border-primary', 'bg-blue-50');
}, false);
});
// Handle file drop
dragDropArea.addEventListener('drop', (e) => {
console.log('File dropped');
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
console.log('File dropped:', files[0].name);
handleFileSelect(files[0]);
}
}, false);
console.log('Banner upload functionality initialized');
} else {
console.error('Could not initialize banner upload: Missing required elements', {
dragDropArea: !!dragDropArea,
uploadImageBtn: !!uploadImageBtn,
bannerImageInput: !!bannerImageInput
});
}
});
// 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();
showNotification('Obrázek byl úspěšně nahrán', 'success');
} 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);
}
// Prevent default drag behaviors
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
/* Reservations Management Section */
// Function to load and display reservations
async function loadReservations() {
const tbody = document.querySelector('#reservationsTable tbody');
try {
const response = await fetch('/api/reservations');
if (!response.ok) throw new Error('Failed to load reservations');
const reservations = await response.json();
window.allReservations = reservations; // Store for filtering
displayReservations(reservations);
updateVehicleFilter(reservations);
} catch (error) {
console.error('Error loading reservations:', error);
tbody.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-4 text-center text-red-500">
Chyba při načítání rezervací: ${error.message}
</td>
</tr>
`;
}
}
// Function to display reservations
function displayReservations(reservations) {
const tbody = document.querySelector('#reservationsTable tbody');
if (!tbody) return;
if (!reservations.length) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
Žádné rezervace k zobrazení
</td>
</tr>
`;
return;
}
// Format date and time as DD.MM.YYYY HH:MM
const formatDateTime = (dateString) => {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}.${month}.${year} ${hours}:${minutes}`;
};
tbody.innerHTML = reservations.map(res => `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">${res.driverName || '-'}</td>
<td class="px-6 py-4">${res.vehicle || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap">${formatDateTime(res.start)}</td>
<td class="px-6 py-4 whitespace-nowrap">${formatDateTime(res.end)}</td>
<td class="px-6 py-4">${res.purpose || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<button onclick="editReservation('${res.id}')" class="text-blue-600 hover:text-blue-900 mr-3">
<i class="fas fa-edit"></i> Upravit
</button>
<button onclick="deleteReservation('${res.id}')" class="text-red-600 hover:text-red-900">
<i class="fas fa-trash"></i> Smazat
</button>
</td>
</tr>
`).join('');
}
// Function to filter reservations
function filterReservations() {
if (!window.allReservations) return;
const vehicleFilter = document.getElementById('vehicleFilter')?.value || '';
const dateFilter = document.getElementById('dateFilter')?.value || '';
let filtered = [...window.allReservations];
// Apply vehicle filter
if (vehicleFilter) {
filtered = filtered.filter(res => res.vehicle && res.vehicle.trim() === vehicleFilter);
}
// Apply date filter
if (dateFilter) {
filtered = filtered.filter(res => {
if (!res.start) return false;
const reservationDate = new Date(res.start);
const filterDate = new Date(dateFilter);
return reservationDate.getFullYear() === filterDate.getFullYear() &&
reservationDate.getMonth() === filterDate.getMonth() &&
reservationDate.getDate() === filterDate.getDate();
});
}
displayReservations(filtered);
}
// Function to export reservations to Excel
function exportReservations() {
if (!window.allReservations || !window.allReservations.length) {
showNotification('Žádné rezervace k exportu', 'warning');
return;
}
// Get filtered reservations
const vehicleFilter = document.getElementById('vehicleFilter').value;
const dateFilter = document.getElementById('dateFilter').value;
let dataToExport = [...window.allReservations];
// Apply filters if set
if (vehicleFilter) {
dataToExport = dataToExport.filter(res => res.vehicle === vehicleFilter);
}
if (dateFilter) {
dataToExport = dataToExport.filter(res => {
const resDate = new Date(res.start).toISOString().split('T')[0];
return resDate === dateFilter;
});
}
// Sort by start date
dataToExport.sort((a, b) => new Date(a.start) - new Date(b.start));
// Format date as DD.MM.YYYY
const formatDate = (dateString) => {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
// Format time as HH:MM
const formatTime = (dateString) => {
const date = new Date(dateString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
};
// Format date and time for Excel (YYYY-MM-DD HH:MM)
const excelDateTime = (dateString) => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
// Create CSV content with semicolon as delimiter for better Excel compatibility
const headers = [
'Řidič',
'Vozidlo',
'Začátek rezervace',
'Konec rezervace',
'Doba trvání',
'Účel cesty'
];
const csvContent = [
headers.join(';'),
...dataToExport.map(res => {
const start = new Date(res.start);
const end = new Date(res.end);
return [
`"${res.driverName || 'Neznámý řidič'}"`,
`"${res.vehicle || 'Neznámé vozidlo'}"`,
`"${excelDateTime(start)}"`,
`"${excelDateTime(end)}"`,
`"${calculateDuration(res)}"`,
`"${res.purpose || 'Nespecifikováno'}"`
].join(';');
})
].join('\r\n');
// Create and trigger download with proper encoding for Excel
const blob = new Blob([
'\ufeff', // UTF-8 BOM for Excel
csvContent
], {
type: 'text/csv;charset=utf-8;'
});
const link = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
link.href = URL.createObjectURL(blob);
link.download = `rezervace_${timestamp}.csv`;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
// Clean up
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}, 100);
}
// Helper function to format date and time
function formatDateTime(date, time) {
return `${date} ${time}`;
}
// Helper function to calculate duration
function calculateDuration(reservation) {
const start = new Date(reservation.start);
const end = new Date(reservation.end);
const diff = end - start;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
let duration = '';
if (days > 0) {
duration += `${days} ${days === 1 ? 'den' : days < 5 ? 'dny' : 'dní'} `;
}
duration += `${hours}h ${minutes}m`;
return duration;
}
// Function to edit a reservation
function editReservation(id) {
// Find the reservation by id
const reservation = window.allReservations.find(r => r.id === id);
if (!reservation) {
showNotification('Rezervaci se nepodařilo najít', 'error');
return;
}
// Redirect to the reservation page with the reservation ID
window.location.href = `/rezervace-aut?edit=${id}`;
}
// Function to delete a reservation
async function deleteReservation(id) {
if (!confirm('Opravdu chcete smazat tuto rezervaci?')) {
return;
}
try {
const response = await fetch(`/api/reservations/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Nepodařilo se smazat rezervaci');
}
// Remove the reservation from the local array and update the display
window.allReservations = window.allReservations.filter(r => r.id !== id);
updateVehicleFilter(window.allReservations);
filterReservations();
showNotification('Rezervace byla úspěšně smazána', 'success');
} catch (error) {
console.error('Error deleting reservation:', error);
showNotification(`Chyba při mazání rezervace: ${error.message}`, 'error');
}
}
// Function to update vehicle filter options
function updateVehicleFilter(reservations) {
const vehicleFilter = document.getElementById('vehicleFilter');
if (!vehicleFilter) return;
// Get unique vehicles from reservations
const vehicles = [...new Set(reservations
.filter(r => r.vehicle) // Filter out undefined/null vehicles
.map(r => r.vehicle.trim()) // Trim whitespace
.filter(Boolean) // Remove any empty strings
)];
// Save current selection
const currentValue = vehicleFilter.value;
// Update options
vehicleFilter.innerHTML = `
<option value="">Všechna vozidla</option>
${vehicles.map(v => `<option value="${v}" ${v === currentValue ? 'selected' : ''}>${v}</option>`).join('')}
`;
}
// Add event listeners when the page loads
document.addEventListener('DOMContentLoaded', function() {
// Initialize filters
const vehicleFilter = document.getElementById('vehicleFilter');
const dateFilter = document.getElementById('dateFilter');
const exportButton = document.getElementById('exportButton');
if (vehicleFilter) {
vehicleFilter.addEventListener('change', filterReservations);
}
if (dateFilter) {
dateFilter.addEventListener('change', filterReservations);
}
if (exportButton) {
exportButton.addEventListener('click', exportReservations);
}
// Load initial data
loadReservations();
});
// Load reservations when page loads
document.addEventListener('DOMContentLoaded', () => {
// ...existing code...
loadReservations();
});
</script>
</body>
</html>