mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-04 04:22:58 +00:00
4973 lines
185 KiB
HTML
4973 lines
185 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>
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></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">
|
|
<div class="flex items-center">
|
|
<button id="sidebarToggle" class="mr-4 text-gray-600 hover:text-gray-900 md:hidden">
|
|
<i class="fas fa-bars text-xl"></i>
|
|
</button>
|
|
<h1>Admin Dashboard</h1>
|
|
</div>
|
|
<button class="logout-btn" id="logoutBtn">Odhlásit se</button>
|
|
</div>
|
|
|
|
<!-- Sidebar Navigation -->
|
|
<div id="sidebar" class="fixed inset-y-0 left-0 z-40 w-64 bg-gray-800 text-white transform -translate-x-full md:translate-x-0 transition-transform duration-300 ease-in-out">
|
|
<div class="p-4 border-b border-gray-700">
|
|
<h2 class="text-xl font-semibold">Administrace</h2>
|
|
</div>
|
|
<nav class="mt-4">
|
|
<a href="#dashboard" id="nav-dashboard" class="nav-link flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors duration-200" data-section="dashboard">
|
|
<i class="fas fa-tachometer-alt w-6"></i>
|
|
<span class="ml-3">Nástěnka</span>
|
|
</a>
|
|
<a href="#reservations" id="nav-reservations" class="nav-link flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors duration-200" data-section="reservations">
|
|
<i class="fas fa-calendar-alt w-6"></i>
|
|
<span class="ml-3">Rezervace</span>
|
|
</a>
|
|
<a href="#banner" id="nav-banner" class="nav-link flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors duration-200" data-section="banner">
|
|
<i class="fas fa-image w-6"></i>
|
|
<span class="ml-3">Banner</span>
|
|
</a>
|
|
<a href="#apps" id="nav-apps" class="nav-link flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors duration-200" data-section="apps">
|
|
<i class="fas fa-th w-6"></i>
|
|
<span class="ml-3">Aplikace</span>
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Overlay for mobile -->
|
|
<div id="sidebarOverlay" class="fixed inset-0 bg-black bg-opacity-50 z-30 hidden md:hidden"></div>
|
|
|
|
<div id="mainContent" class="container md:ml-64 transition-all duration-300">
|
|
<!-- Dashboard Section -->
|
|
<div id="dashboard" class="section-content" data-section="dashboard">
|
|
<h2 class="section-title">Vítejte v administraci</h2>
|
|
|
|
<!-- Apps Management Section -->
|
|
<div class="card" style="margin: 2rem auto; max-width: 1000px;">
|
|
</div>
|
|
|
|
<!-- Apps Section -->
|
|
<div id="apps" class="card section-content" data-section="apps" style="margin: 2rem auto; max-width: 1000px; display: none;">
|
|
<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">
|
|
</div>
|
|
|
|
<!-- Banner Section -->
|
|
<div id="banner" class="card section-content" data-section="banner" style="margin: 2rem auto; max-width: 1200px; display: none;">
|
|
<h3>Nastavení 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>
|
|
|
|
<!-- Reservations Section -->
|
|
<div id="reservations" class="card section-content" data-section="reservations" style="margin: 2rem auto; max-width: 1200px; display: none;">
|
|
<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>
|
|
|
|
<!-- Edit Reservation Modal -->
|
|
<div id="editReservationModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black opacity-50"></div>
|
|
<div class="fixed inset-0 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl">
|
|
<div class="p-6">
|
|
<h3 class="text-xl font-semibold mb-4">Upravit rezervaci</h3>
|
|
<form id="editReservationForm" class="space-y-4">
|
|
<input type="hidden" id="editReservationId">
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="editDriverName" class="block text-sm font-medium text-gray-700">Řidič *</label>
|
|
<input type="text" id="editDriverName" required
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
</div>
|
|
<div>
|
|
<label for="editVehicle" class="block text-sm font-medium text-gray-700">Vozidlo *</label>
|
|
<select id="editVehicle" required
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
<option value="">Vyberte vozidlo</option>
|
|
<!-- Options will be populated by JavaScript -->
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="editStartDate" class="block text-sm font-medium text-gray-700">Datum od *</label>
|
|
<input type="date" id="editStartDate" required
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
</div>
|
|
<div>
|
|
<label for="editStartTime" class="block text-sm font-medium text-gray-700">Čas od *</label>
|
|
<input type="time" id="editStartTime" required
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="editEndDate" class="block text-sm font-medium text-gray-700">Datum do *</label>
|
|
<input type="date" id="editEndDate" required
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
</div>
|
|
<div>
|
|
<label for="editEndTime" class="block text-sm font-medium text-gray-700">Čas do *</label>
|
|
<input type="time" id="editEndTime" required
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="editPurpose" class="block text-sm font-medium text-gray-700">Účel cesty</label>
|
|
<textarea id="editPurpose" rows="3"
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 pt-4">
|
|
<button type="button" onclick="closeEditModal()"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm 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="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
Uložit změny
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</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 mb-3">Vyberte ikonu</h3>
|
|
<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, ' ');
|
|
}
|
|
});
|
|
});
|
|
|
|
// 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');
|
|
}
|
|
}
|
|
|
|
// Navigation functionality
|
|
const sidebar = document.getElementById('sidebar');
|
|
const sidebarToggle = document.getElementById('sidebarToggle');
|
|
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
|
const mainContent = document.getElementById('mainContent');
|
|
|
|
// Toggle sidebar on mobile
|
|
function toggleSidebar() {
|
|
sidebar.classList.toggle('-translate-x-full');
|
|
sidebarOverlay.classList.toggle('hidden');
|
|
document.body.classList.toggle('overflow-hidden');
|
|
}
|
|
|
|
// Close sidebar when clicking outside on mobile
|
|
function closeSidebar() {
|
|
if (!sidebar.classList.contains('-translate-x-full')) {
|
|
toggleSidebar();
|
|
}
|
|
}
|
|
|
|
// Navigation between sections
|
|
function showSection(sectionId) {
|
|
// If no sectionId provided, try to get it from URL hash
|
|
if (!sectionId) {
|
|
const hash = window.location.hash.substring(1);
|
|
if (hash && ['dashboard', 'reservations', 'banner', 'apps'].includes(hash)) {
|
|
sectionId = hash;
|
|
} else {
|
|
sectionId = 'dashboard';
|
|
}
|
|
} else {
|
|
// Update URL hash without page jump
|
|
history.pushState(null, null, `#${sectionId}`);
|
|
}
|
|
|
|
// Hide all sections
|
|
document.querySelectorAll('.section-content').forEach(section => {
|
|
section.style.display = 'none';
|
|
});
|
|
|
|
// Show selected section
|
|
const section = document.getElementById(sectionId) || document.querySelector(`.section-content[data-section="${sectionId}"]`);
|
|
if (section) {
|
|
section.style.display = 'block';
|
|
// Smooth scroll to the top of the section
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
// Update active nav link
|
|
document.querySelectorAll('nav a').forEach(link => {
|
|
link.classList.remove('bg-gray-700', 'text-white');
|
|
link.classList.add('text-gray-300');
|
|
});
|
|
|
|
const activeLink = document.getElementById(`nav-${sectionId}`) || document.querySelector(`nav a[data-section="${sectionId}"]`);
|
|
if (activeLink) {
|
|
activeLink.classList.remove('text-gray-300');
|
|
activeLink.classList.add('bg-gray-700', 'text-white');
|
|
}
|
|
|
|
// Close sidebar on mobile after selection
|
|
if (window.innerWidth < 768) {
|
|
closeSidebar();
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
if (sidebarToggle) {
|
|
sidebarToggle.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
toggleSidebar();
|
|
});
|
|
}
|
|
|
|
if (sidebarOverlay) {
|
|
sidebarOverlay.addEventListener('click', closeSidebar);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Handle back/forward navigation
|
|
window.addEventListener('popstate', function() {
|
|
const hash = window.location.hash.substring(1);
|
|
if (hash) {
|
|
showSection(hash);
|
|
} else {
|
|
showSection('dashboard');
|
|
}
|
|
});
|
|
|
|
// Show section based on URL hash or default to dashboard
|
|
const hash = window.location.hash.substring(1);
|
|
if (hash && ['dashboard', 'reservations', 'banner', 'apps'].includes(hash)) {
|
|
showSection(hash);
|
|
} else {
|
|
showSection('dashboard');
|
|
}
|
|
|
|
// Set up navigation links
|
|
document.querySelectorAll('nav a').forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const sectionId = link.getAttribute('data-section');
|
|
showSection(sectionId);
|
|
});
|
|
});
|
|
|
|
// Handle window resize
|
|
function handleResize() {
|
|
if (window.innerWidth >= 768) {
|
|
sidebar.classList.remove('-translate-x-full');
|
|
sidebarOverlay.classList.add('hidden');
|
|
document.body.classList.remove('overflow-hidden');
|
|
}
|
|
}
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
handleResize();
|
|
});
|
|
|
|
// 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 XLSX
|
|
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 && res.vehicle.trim() === vehicleFilter);
|
|
}
|
|
if (dateFilter) {
|
|
dataToExport = dataToExport.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();
|
|
});
|
|
}
|
|
|
|
if (!dataToExport.length) {
|
|
showNotification('Žádná data k exportu po aplikování filtrů', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Sort by start date
|
|
dataToExport.sort((a, b) => new Date(a.start) - new Date(b.start));
|
|
|
|
// Format date and time as DD.MM.YYYY HH:MM
|
|
const formatDateTime = (dateString) => {
|
|
if (!dateString) return '';
|
|
try {
|
|
const date = new Date(dateString);
|
|
if (isNaN(date.getTime())) return '';
|
|
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}`;
|
|
} catch (e) {
|
|
console.error('Error formatting date:', e);
|
|
return '';
|
|
}
|
|
};
|
|
|
|
// Prepare data for XLSX
|
|
const headers = ['Řidič', 'Vozidlo', 'Od', 'Do', 'Účel'];
|
|
|
|
// Convert data to worksheet
|
|
const wsData = [
|
|
headers,
|
|
...dataToExport.map(res => [
|
|
res.driverName || '',
|
|
res.vehicle || '',
|
|
formatDateTime(res.start) || '',
|
|
formatDateTime(res.end) || '',
|
|
res.purpose || ''
|
|
])
|
|
];
|
|
|
|
// Create worksheet
|
|
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
|
|
|
// Set column widths
|
|
const colWidths = [
|
|
{ wch: 20 }, // Řidič
|
|
{ wch: 25 }, // Vozidlo
|
|
{ wch: 20 }, // Od
|
|
{ wch: 20 }, // Do
|
|
{ wch: 40 } // Účel
|
|
];
|
|
ws['!cols'] = colWidths;
|
|
|
|
// Create workbook
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Rezervace');
|
|
|
|
// Generate XLSX file
|
|
const timestamp = new Date().toISOString().split('T')[0];
|
|
XLSX.writeFile(wb, `rezervace_${timestamp}.xlsx`);
|
|
}
|
|
|
|
// 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 open edit modal with reservation data
|
|
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;
|
|
}
|
|
|
|
// Format dates for the form
|
|
const startDate = new Date(reservation.start);
|
|
const endDate = new Date(reservation.end);
|
|
|
|
// Populate the form
|
|
document.getElementById('editReservationId').value = reservation.id;
|
|
document.getElementById('editDriverName').value = reservation.driverName || '';
|
|
|
|
// Set vehicle select
|
|
const vehicleSelect = document.getElementById('editVehicle');
|
|
if (reservation.vehicle) {
|
|
// Check if the vehicle exists in the select, if not add it
|
|
let optionExists = false;
|
|
for (let i = 0; i < vehicleSelect.options.length; i++) {
|
|
if (vehicleSelect.options[i].value === reservation.vehicle) {
|
|
optionExists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!optionExists) {
|
|
const option = document.createElement('option');
|
|
option.value = reservation.vehicle;
|
|
option.textContent = reservation.vehicle;
|
|
vehicleSelect.appendChild(option);
|
|
}
|
|
vehicleSelect.value = reservation.vehicle;
|
|
}
|
|
|
|
// Set dates and times
|
|
document.getElementById('editStartDate').value = startDate.toISOString().split('T')[0];
|
|
document.getElementById('editStartTime').value =
|
|
`${String(startDate.getHours()).padStart(2, '0')}:${String(startDate.getMinutes()).padStart(2, '0')}`;
|
|
document.getElementById('editEndDate').value = endDate.toISOString().split('T')[0];
|
|
document.getElementById('editEndTime').value =
|
|
`${String(endDate.getHours()).padStart(2, '0')}:${String(endDate.getMinutes()).padStart(2, '0')}`;
|
|
|
|
document.getElementById('editPurpose').value = reservation.purpose || '';
|
|
|
|
// Show the modal
|
|
document.getElementById('editReservationModal').classList.remove('hidden');
|
|
}
|
|
|
|
// Function to delete a reservation
|
|
async function deleteReservation(id) {
|
|
if (!confirm('Opravdu chcete smazat tuto rezervaci?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/reservations/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'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('')}
|
|
`;
|
|
}
|
|
|
|
// Function to close the edit modal
|
|
function closeEditModal() {
|
|
document.getElementById('editReservationModal').classList.add('hidden');
|
|
}
|
|
|
|
// Function to save reservation changes
|
|
async function saveReservation(event) {
|
|
event.preventDefault();
|
|
|
|
const id = document.getElementById('editReservationId').value;
|
|
const driverName = document.getElementById('editDriverName').value.trim();
|
|
const vehicle = document.getElementById('editVehicle').value;
|
|
const startDate = document.getElementById('editStartDate').value;
|
|
const startTime = document.getElementById('editStartTime').value;
|
|
const endDate = document.getElementById('editEndDate').value;
|
|
const endTime = document.getElementById('editEndTime').value;
|
|
const purpose = document.getElementById('editPurpose').value.trim();
|
|
|
|
// Validate required fields
|
|
if (!driverName || !vehicle || !startDate || !startTime || !endDate || !endTime) {
|
|
showNotification('Vyplňte prosím všechna povinná pole', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/reservations/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
driverName,
|
|
vehicle,
|
|
startDate,
|
|
startTime,
|
|
endDate,
|
|
endTime,
|
|
purpose
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const result = await response.json();
|
|
throw new Error(result.error || 'Nepodařilo se uložit změny');
|
|
}
|
|
|
|
// Update the reservation in the local array
|
|
const index = window.allReservations.findIndex(r => r.id === id);
|
|
if (index !== -1) {
|
|
window.allReservations[index] = {
|
|
...window.allReservations[index],
|
|
driverName,
|
|
vehicle,
|
|
startDate,
|
|
startTime,
|
|
endDate,
|
|
endTime,
|
|
purpose
|
|
};
|
|
|
|
// Update the display
|
|
filterReservations();
|
|
updateVehicleFilter(window.allReservations);
|
|
showNotification('Rezervace byla úspěšně aktualizována', 'success');
|
|
closeEditModal();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating reservation:', error);
|
|
showNotification(`Chyba při ukládání změn: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
const editForm = document.getElementById('editReservationForm');
|
|
|
|
if (vehicleFilter) {
|
|
vehicleFilter.addEventListener('change', filterReservations);
|
|
}
|
|
|
|
if (dateFilter) {
|
|
dateFilter.addEventListener('change', filterReservations);
|
|
}
|
|
|
|
if (exportButton) {
|
|
exportButton.addEventListener('click', exportReservations);
|
|
}
|
|
|
|
if (editForm) {
|
|
editForm.addEventListener('submit', saveReservation);
|
|
}
|
|
|
|
// Populate vehicle dropdown in edit form
|
|
const vehicleSelect = document.getElementById('editVehicle');
|
|
if (vehicleSelect) {
|
|
const vehicles = ['VW Caddy', 'VW Golf', 'Škoda Fabia', 'BMW 218d', 'Škoda Superb'];
|
|
vehicles.forEach(vehicle => {
|
|
const option = document.createElement('option');
|
|
option.value = vehicle;
|
|
option.textContent = vehicle;
|
|
vehicleSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Load initial data
|
|
loadReservations();
|
|
});
|
|
|
|
// Load reservations when page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadReservations();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |