nordabiz/docs/plans/2026-02-18-company-edit-wysiwyg-preview.md

28 KiB

Company Edit WYSIWYG + Live Preview — Implementation Plan

Status: UKOŃCZONY (2026-02-18)

Goal: Dodać edytor wizualny Quill.js i panel podglądu na żywo do formularza edycji profilu firmy.

Architecture: Split-view layout (60% edytor / 40% preview) na desktopie, mobile bottom sheet. Quill.js v2 z CDN jako WYSIWYG dla 4 pól tekstowych (description_full, founding_history, core_values, services_offered). Preview panel ze sticky positioning, aktualizowany live via Quill text-change event.

Tech Stack: Quill.js v2 (CDN), Vanilla JS, CSS Grid, Jinja2

Staging: Wszystkie zmiany testowane na staging.nordabiznes.pl


Ważne konteksty

Blokady template w base.html

  • {% block extra_css %} jest WEWNĄTRZ <style> (linia 1251)
  • {% block extra_js %} jest WEWNĄTRZ <script> (linia 2024)
  • NIE MOŻNA dodać <link> ani <script src> w tych blokach!
  • Rozwiązanie: nowy {% block head_extra %} po </style> w base.html

Pola z WYSIWYG

Pole Zakładka Obecny element
description_full Opis <textarea id="description_full" rows="10">
founding_history Opis <textarea id="founding_history" rows="5">
core_values Opis <textarea id="core_values" rows="4">
services_offered Usługi <textarea id="services_offered" rows="8">

Pola BEZ WYSIWYG (bez zmian)

  • description_short (plain text, 500 znaków)
  • technologies_used, operational_area, languages_offered (krótkie pola)

Task 1: Dodać {% block head_extra %} do base.html

Files:

  • Modify: templates/base.html:1252 (po </style>, przed </head>)

Step 1: Dodaj nowy block w base.html

W templates/base.html, po linii 1252 (</style>), przed linią z analytics script, dodaj:

    {% block head_extra %}{% endblock %}

To pozwala child templates dodawać zewnętrzne <link> i <script> tagi.

Step 2: Zweryfikuj że nic się nie zepsuło

Uruchom lokalnie: python3 app.py i sprawdź główną stronę / — nowy empty block nie powinien nic zmienić.

Step 3: Commit

git add templates/base.html
git commit -m "feat: add head_extra block to base.html for external CSS/JS"

Task 2: Załadować Quill.js CDN w company_edit.html

Files:

  • Modify: templates/company_edit.html (dodać block head_extra)

Step 1: Dodaj block head_extra z Quill CDN

Na początku pliku, po {% block title %}...{% endblock %} a przed {% block extra_css %}, dodaj:

{% block head_extra %}
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
{% endblock %}

Step 2: Zweryfikuj ładowanie

Uruchom lokalnie, otwórz /firma/edytuj/<id>, w DevTools Console wpisz typeof Quill — powinno zwrócić "function".

Step 3: Commit

git add templates/company_edit.html
git commit -m "feat: load Quill.js CDN in company edit template"

Task 3: Zmienić layout na grid 60/40 z panelem preview

Files:

  • Modify: templates/company_edit.html (CSS + HTML structure)

Step 1: Zmień CSS layout

W sekcji {% block extra_css %}, zmień:

.ce-container {
    max-width: 860px;
    margin: 0 auto;
    padding: var(--spacing-md) var(--spacing-lg);
}

na:

.ce-container {
    max-width: 1400px;
    margin: 0 auto;
    padding: var(--spacing-md) var(--spacing-lg);
}

/* Split layout: editor + preview */
.ce-layout {
    display: grid;
    grid-template-columns: 1fr 380px;
    gap: var(--spacing-lg);
    align-items: start;
}

/* Preview panel */
.ce-preview {
    position: sticky;
    top: 80px;
    max-height: calc(100vh - 100px);
    overflow-y: auto;
    background: var(--surface, #fff);
    border-radius: var(--radius-lg, 0.75rem);
    box-shadow: var(--shadow);
    padding: var(--spacing-lg);
}
.ce-preview-title {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: var(--spacing-md);
    padding-bottom: var(--spacing-sm);
    border-bottom: 1px solid var(--border, #e0e4eb);
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
}
.ce-preview-title svg { width: 16px; height: 16px; }

/* Preview sections - match company_detail.html styling */
.preview-company-name {
    font-size: var(--font-size-xl, 1.25rem);
    font-weight: 700;
    color: var(--text-primary, #303030);
    margin-bottom: var(--spacing-sm);
}
.preview-short-desc {
    font-size: var(--font-size-sm);
    color: var(--text-secondary, #464646);
    margin-bottom: var(--spacing-lg);
    font-style: italic;
}
.preview-section {
    margin-bottom: var(--spacing-lg);
    padding-bottom: var(--spacing-md);
    border-bottom: 1px solid var(--border, #e0e4eb);
}
.preview-section:last-child {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
}
.preview-section-label {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--primary, #2E4872);
    margin-bottom: var(--spacing-sm);
}
.preview-section-content {
    font-size: var(--font-size-sm);
    color: var(--text-primary, #303030);
    line-height: 1.7;
}
.preview-section-content p { margin-bottom: var(--spacing-sm); }
.preview-section-content ul, .preview-section-content ol {
    padding-left: var(--spacing-lg);
    margin-bottom: var(--spacing-sm);
}
.preview-section-content li { margin-bottom: 2px; }
.preview-section-content a { color: var(--primary, #2E4872); }
.preview-section-content strong { font-weight: 600; }
.preview-empty {
    color: var(--text-secondary, #464646);
    font-style: italic;
    font-size: var(--font-size-sm);
    opacity: 0.6;
}

/* Preview contact items */
.preview-contact-item {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    font-size: var(--font-size-sm);
    margin-bottom: var(--spacing-xs);
}
.preview-contact-item svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; }

/* Preview social icons */
.preview-social-item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--font-size-sm);
    color: var(--primary, #2E4872);
    margin-right: var(--spacing-md);
    margin-bottom: var(--spacing-xs);
}

Step 2: Dodaj responsive CSS (mobile)

Zamień istniejący @media (max-width: 768px) na:

@media (max-width: 1024px) {
    .ce-layout {
        grid-template-columns: 1fr;
    }
    .ce-preview {
        display: none;
    }
    .ce-preview-mobile-btn {
        display: flex;
    }
}
@media (max-width: 768px) {
    .ce-header { flex-direction: column; text-align: center; }
    .ce-header-actions { margin-left: 0; }
    .ce-card .form-row { grid-template-columns: 1fr; }
    .contact-row, .social-row { flex-wrap: wrap; }
    .contact-type-select, .social-platform-select { flex: 1 1 100%; }
    .contact-purpose-input { flex: 1 1 100%; }
    .ce-tab { padding: var(--spacing-sm) var(--spacing-md); font-size: 13px; }
    .ce-tab span.tab-label { display: none; }
}

Dodaj CSS dla mobile preview button i bottom sheet:

/* Mobile preview button & bottom sheet */
.ce-preview-mobile-btn {
    display: none;
    position: fixed;
    bottom: var(--spacing-lg);
    right: var(--spacing-lg);
    z-index: 900;
    padding: 12px 20px;
    background: var(--primary, #2E4872);
    color: white;
    border: none;
    border-radius: 50px;
    font-size: var(--font-size-sm);
    font-weight: 600;
    font-family: var(--font-family);
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(46, 72, 114, 0.3);
    align-items: center;
    gap: var(--spacing-sm);
    transition: all 0.2s;
}
.ce-preview-mobile-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(46, 72, 114, 0.4);
}
.ce-preview-mobile-btn svg { width: 18px; height: 18px; }

.ce-preview-sheet-overlay {
    display: none;
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.5);
    z-index: 1100;
}
.ce-preview-sheet-overlay.active { display: block; }
.ce-preview-sheet {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    max-height: 80vh;
    background: var(--surface, #fff);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
    z-index: 1200;
    overflow-y: auto;
    padding: var(--spacing-lg);
    transform: translateY(100%);
    transition: transform 0.3s ease;
}
.ce-preview-sheet-overlay.active .ce-preview-sheet {
    transform: translateY(0);
}
.ce-preview-sheet-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: var(--spacing-md);
    padding-bottom: var(--spacing-sm);
    border-bottom: 1px solid var(--border, #e0e4eb);
}
.ce-preview-sheet-close {
    background: none;
    border: 1px solid var(--border, #e0e4eb);
    border-radius: var(--radius);
    padding: 6px 12px;
    cursor: pointer;
    font-size: var(--font-size-sm);
    font-family: var(--font-family);
    color: var(--text-secondary);
}

Step 3: Zmień HTML structure

W {% block content %}, zamień fragment od <!-- Main card --> do końca formularza. Owiń .ce-card i nowy .ce-preview w .ce-layout:

<!-- Layout: Editor + Preview -->
<div class="ce-layout">

    <!-- Left: Edit form -->
    <div class="ce-card">
        <!-- Tabs -->
        ...istniejące tabs i form bez zmian...
    </div>

    <!-- Right: Live Preview -->
    <div class="ce-preview" id="livePreview">
        <div class="ce-preview-title">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
            Podgląd profilu
        </div>
        <div class="preview-company-name">{{ company.name }}</div>
        <div class="preview-short-desc" id="previewShortDesc">{{ company.description_short or 'Brak krótkiego opisu' }}</div>

        <div class="preview-section" id="previewDescriptionSection">
            <div class="preview-section-label">Opis firmy</div>
            <div class="preview-section-content" id="previewDescFull">
                {% if company.description_full %}{{ company.description_full | safe }}{% else %}<span class="preview-empty">Uzupełnij opis firmy...</span>{% endif %}
            </div>
        </div>

        <div class="preview-section" id="previewHistorySection">
            <div class="preview-section-label">Historia i doświadczenie</div>
            <div class="preview-section-content" id="previewHistory">
                {% if company.founding_history %}{{ company.founding_history | safe }}{% else %}<span class="preview-empty">Uzupełnij historię...</span>{% endif %}
            </div>
        </div>

        <div class="preview-section" id="previewValuesSection">
            <div class="preview-section-label">Wartości i misja</div>
            <div class="preview-section-content" id="previewValues">
                {% if company.core_values %}{{ company.core_values | safe }}{% else %}<span class="preview-empty">Uzupełnij wartości...</span>{% endif %}
            </div>
        </div>

        <div class="preview-section" id="previewServicesSection">
            <div class="preview-section-label">Oferowane usługi</div>
            <div class="preview-section-content" id="previewServices">
                {% if company.services_offered %}{{ company.services_offered | safe }}{% else %}<span class="preview-empty">Uzupełnij usługi...</span>{% endif %}
            </div>
        </div>

        <div class="preview-section" id="previewContactSection">
            <div class="preview-section-label">Dane kontaktowe</div>
            <div class="preview-section-content" id="previewContact">
                {% if company.email %}<div class="preview-contact-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>{{ company.email }}</div>{% endif %}
                {% if company.phone %}<div class="preview-contact-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>{{ company.phone }}</div>{% endif %}
                {% if company.website %}<div class="preview-contact-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>{{ company.website }}</div>{% endif %}
            </div>
        </div>

        <div class="preview-section" id="previewSocialSection">
            <div class="preview-section-label">Social Media</div>
            <div class="preview-section-content" id="previewSocial">
                {% for sm in social_media %}
                <span class="preview-social-item">{{ sm.platform | capitalize }}</span>
                {% endfor %}
                {% if not social_media %}<span class="preview-empty">Brak profili social media</span>{% endif %}
            </div>
        </div>
    </div>

</div><!-- /.ce-layout -->

<!-- Mobile preview button -->
<button type="button" class="ce-preview-mobile-btn" id="mobilePreviewBtn" onclick="openMobilePreview()">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
    Podgląd
</button>

<!-- Mobile preview sheet -->
<div class="ce-preview-sheet-overlay" id="previewSheetOverlay">
    <div class="ce-preview-sheet" id="previewSheet">
        <div class="ce-preview-sheet-header">
            <span style="font-weight: 600;">Podgląd profilu</span>
            <button type="button" class="ce-preview-sheet-close" onclick="closeMobilePreview()">Zamknij</button>
        </div>
        <div id="mobilePreviewContent">
            <!-- Populated dynamically from desktop preview -->
        </div>
    </div>
</div>

Step 4: Zweryfikuj layout

Uruchom lokalnie, otwórz /firma/edytuj/<id>:

  • Desktop: formularz po lewej (60%), preview po prawej (40%)
  • Resize do <1024px: preview znika, pojawia się floating button "Podgląd"

Step 5: Commit

git add templates/company_edit.html
git commit -m "feat: split layout with live preview panel for company edit"

Task 4: Zamienić textareas na Quill.js edytory

Files:

  • Modify: templates/company_edit.html (HTML formularza + JS inicjalizacja)

Step 1: Dodaj CSS dla Quill kontenerów

W sekcji {% block extra_css %} dodaj:

/* Quill editor overrides */
.quill-container {
    border: 1px solid var(--border, #e0e4eb);
    border-radius: var(--radius, 0.5rem);
    overflow: hidden;
    background: var(--surface, #fff);
    transition: var(--transition);
}
.quill-container:focus-within {
    border-color: var(--primary, #2E4872);
    box-shadow: 0 0 0 3px rgba(46, 72, 114, 0.1);
}
.quill-container .ql-toolbar {
    border: none !important;
    border-bottom: 1px solid var(--border, #e0e4eb) !important;
    background: var(--background, #EDF0F5);
    font-family: var(--font-family) !important;
}
.quill-container .ql-container {
    border: none !important;
    font-family: var(--font-family) !important;
    font-size: var(--font-size-base, 1rem) !important;
}
.quill-container .ql-editor {
    min-height: 120px;
    line-height: 1.7;
    color: var(--text-primary, #303030);
}
.quill-container .ql-editor.ql-blank::before {
    color: var(--text-secondary, #464646);
    opacity: 0.5;
    font-style: italic;
}
.quill-container.quill-tall .ql-editor {
    min-height: 200px;
}
fieldset[disabled] .quill-container {
    opacity: 0.5;
    pointer-events: none;
}

Step 2: Zamień textarea na Quill kontener + hidden textarea dla description_full

Zastąp obecny fragment:

<div class="form-group">
    <label for="description_full" class="form-label">Pełny opis działalności</label>
    <textarea id="description_full" name="description_full" class="form-input" rows="10" placeholder="Szczegółowy opis tego czym zajmuje się firma, jakie ma doświadczenie i co ją wyróżnia...">{{ company.description_full or '' }}</textarea>
    <p class="form-help">Główny opis na stronie profilu firmy. Dozwolone tagi HTML: &lt;p&gt;, &lt;strong&gt;, &lt;em&gt;, &lt;ul&gt;, &lt;li&gt;, &lt;a&gt;</p>
</div>

na:

<div class="form-group">
    <label class="form-label">Pełny opis działalności</label>
    <div class="quill-container quill-tall" id="quill-description_full"></div>
    <textarea id="description_full" name="description_full" style="display:none;">{{ company.description_full or '' }}</textarea>
    <p class="form-help">Użyj paska narzędzi do formatowania tekstu</p>
</div>

Step 3: Powtórz dla founding_history, core_values, services_offered

Analogicznie zamień textareas na Quill kontenery dla:

founding_history:

<div class="form-group">
    <label class="form-label">Historia i doświadczenie</label>
    <div class="quill-container" id="quill-founding_history"></div>
    <textarea id="founding_history" name="founding_history" style="display:none;">{{ company.founding_history or '' }}</textarea>
</div>

core_values:

<div class="form-group">
    <label class="form-label">Wartości i misja</label>
    <div class="quill-container" id="quill-core_values"></div>
    <textarea id="core_values" name="core_values" style="display:none;">{{ company.core_values or '' }}</textarea>
</div>

services_offered (w zakładce Usługi):

<div class="form-group">
    <label class="form-label">Oferowane usługi i produkty</label>
    <div class="quill-container quill-tall" id="quill-services_offered"></div>
    <textarea id="services_offered" name="services_offered" style="display:none;">{{ company.services_offered or '' }}</textarea>
    <p class="form-help">Lista usług pomagająca klientom znaleźć Twoją firmę</p>
</div>

Step 4: Dodaj JS inicjalizacji Quill w {% block extra_js %}

Na początku bloku extra_js (przed istniejącym kodem tab switching), dodaj:

// ============================================
// Quill.js WYSIWYG Initialization
// ============================================
var quillInstances = {};
var QUILL_TOOLBAR = [
    ['bold', 'italic'],
    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
    ['link'],
    ['clean']
];

function initQuillEditor(fieldName, placeholder) {
    var container = document.getElementById('quill-' + fieldName);
    var textarea = document.getElementById(fieldName);
    if (!container || !textarea) return null;

    var quill = new Quill(container, {
        theme: 'snow',
        modules: { toolbar: QUILL_TOOLBAR },
        placeholder: placeholder || 'Wpisz tekst...'
    });

    // Load existing content from textarea
    var existing = textarea.value.trim();
    if (existing) {
        quill.root.innerHTML = existing;
    }

    // Sync to hidden textarea on every change
    quill.on('text-change', function() {
        var html = quill.root.innerHTML;
        // Quill sets empty content as <p><br></p>
        textarea.value = (html === '<p><br></p>') ? '' : html;
        updatePreview(fieldName, textarea.value);
    });

    quillInstances[fieldName] = quill;
    return quill;
}

// Initialize all Quill editors (only if Quill loaded)
if (typeof Quill !== 'undefined') {
    initQuillEditor('description_full', 'Szczegółowy opis tego czym zajmuje się firma...');
    initQuillEditor('founding_history', 'Kiedy firma powstała, jakie ma doświadczenie...');
    initQuillEditor('core_values', 'Kluczowe wartości firmy, misja...');
    initQuillEditor('services_offered', 'Wymień główne usługi i produkty...');
}

Step 5: Zweryfikuj edytor

Uruchom lokalnie:

  • 4 pola mają pasek narzędzi (Bold, Italic, Lista, Link, Wyczyść)
  • Istniejąca treść jest załadowana
  • Po edycji i kliknięciu "Zapisz" — treść zapisuje się poprawnie (hidden textarea sync)

Step 6: Commit

git add templates/company_edit.html
git commit -m "feat: replace textareas with Quill.js WYSIWYG editors"

Task 5: Podłączyć live preview do edytorów

Files:

  • Modify: templates/company_edit.html (JS w bloku extra_js)

Step 1: Dodaj funkcję updatePreview i podłącz zwykłe pola

W bloku extra_js, dodaj po inicjalizacji Quill:

// ============================================
// Live Preview Updates
// ============================================
var previewDebounceTimers = {};

function updatePreview(fieldName, value) {
    clearTimeout(previewDebounceTimers[fieldName]);
    previewDebounceTimers[fieldName] = setTimeout(function() {
        doUpdatePreview(fieldName, value);
    }, 300);
}

function doUpdatePreview(fieldName, value) {
    var mapping = {
        'description_short': 'previewShortDesc',
        'description_full': 'previewDescFull',
        'founding_history': 'previewHistory',
        'core_values': 'previewValues',
        'services_offered': 'previewServices'
    };

    var emptyTexts = {
        'description_short': 'Brak krótkiego opisu',
        'description_full': 'Uzupełnij opis firmy...',
        'founding_history': 'Uzupełnij historię...',
        'core_values': 'Uzupełnij wartości...',
        'services_offered': 'Uzupełnij usługi...'
    };

    var targetId = mapping[fieldName];
    if (!targetId) return;
    var el = document.getElementById(targetId);
    if (!el) return;

    if (value && value.trim() && value !== '<p><br></p>') {
        el.innerHTML = value;
        el.classList.remove('preview-empty');
    } else {
        el.innerHTML = '<span class="preview-empty">' + (emptyTexts[fieldName] || '') + '</span>';
    }
}

// Hook plain text fields to preview
var shortDescField = document.getElementById('description_short');
if (shortDescField) {
    shortDescField.addEventListener('input', function() {
        updatePreview('description_short', this.value);
    });
}

// Hook contact fields to preview
function updateContactPreview() {
    var email = (document.getElementById('email') || {}).value || '';
    var phone = (document.getElementById('phone') || {}).value || '';
    var el = document.getElementById('previewContact');
    if (!el) return;
    var html = '';
    if (email) html += '<div class="preview-contact-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>' + email + '</div>';
    if (phone) html += '<div class="preview-contact-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>' + phone + '</div>';
    el.innerHTML = html || '<span class="preview-empty">Brak danych kontaktowych</span>';
}

['email', 'phone'].forEach(function(id) {
    var field = document.getElementById(id);
    if (field) field.addEventListener('input', updateContactPreview);
});

Step 2: Dodaj logikę podświetlania aktywnej sekcji w preview

Rozszerz istniejący tab switching handler — gdy użytkownik zmienia zakładkę, podświetl odpowiednią sekcję preview:

// Highlight preview section matching active tab
function highlightPreviewTab(tabName) {
    var sections = document.querySelectorAll('.ce-preview .preview-section');
    sections.forEach(function(s) { s.style.opacity = '0.4'; s.style.transition = 'opacity 0.3s'; });

    var tabToSections = {
        'description': ['previewDescriptionSection', 'previewHistorySection', 'previewValuesSection'],
        'services': ['previewServicesSection'],
        'contacts': ['previewContactSection'],
        'social': ['previewSocialSection']
    };

    var active = tabToSections[tabName] || [];
    active.forEach(function(id) {
        var el = document.getElementById(id);
        if (el) el.style.opacity = '1';
    });

    // Visibility tab — show all
    if (tabName === 'visibility') {
        sections.forEach(function(s) { s.style.opacity = '1'; });
    }
}

W istniejącym tab switching callback (w tabs.forEach(function(tab)...), dodaj wywołanie highlightPreviewTab(target) po przełączeniu.

Step 3: Dodaj mobile preview JS

// Mobile preview
function openMobilePreview() {
    var mobileContent = document.getElementById('mobilePreviewContent');
    var desktopPreview = document.getElementById('livePreview');
    if (mobileContent && desktopPreview) {
        // Clone preview content (skip the title)
        var clone = desktopPreview.cloneNode(true);
        var title = clone.querySelector('.ce-preview-title');
        if (title) title.remove();
        mobileContent.innerHTML = clone.innerHTML;
    }
    document.getElementById('previewSheetOverlay').classList.add('active');
    document.body.style.overflow = 'hidden';
}

function closeMobilePreview() {
    document.getElementById('previewSheetOverlay').classList.remove('active');
    document.body.style.overflow = '';
}

// Close on overlay click
var overlay = document.getElementById('previewSheetOverlay');
if (overlay) {
    overlay.addEventListener('click', function(e) {
        if (e.target === this) closeMobilePreview();
    });
}

Step 4: Zweryfikuj preview

  • Wpisz tekst w WYSIWYG → preview aktualizuje się po 300ms
  • Zmień email/telefon → preview kontaktu się aktualizuje
  • Przełącz zakładki → odpowiednie sekcje preview się podświetlają
  • Na mobile → przycisk "Podgląd" otwiera bottom sheet z aktualną treścią

Step 5: Commit

git add templates/company_edit.html
git commit -m "feat: connect live preview to WYSIWYG editors and form fields"

Task 6: Deploy na staging i weryfikacja

Files: Brak zmian w plikach

Step 1: Push do repozytoriów

git push origin master && git push inpi master

Step 2: Deploy na staging

ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"

Step 3: Weryfikacja na staging

Otwórz https://staging.nordabiznes.pl w przeglądarce:

  1. Zaloguj się jako admin/manager
  2. Przejdź do /firma/edytuj/<id> dowolnej firmy
  3. Sprawdź:
    • Quill toolbar widoczny dla 4 pól (opis, historia, wartości, usługi)
    • Istniejąca treść załadowana w edytorach
    • Formatowanie działa (bold, italic, listy, linki)
    • Preview panel po prawej stronie (desktop)
    • Preview aktualizuje się live podczas pisania
    • Przełączanie zakładek podświetla sekcje preview
    • Zapis formularza działa poprawnie (treść z Quill trafia do bazy)
    • Po zapisie i ponownym otwarciu — formatowanie zachowane
    • Na mobile (<1024px): preview ukryty, floating button "Podgląd" widoczny
    • Kliknięcie "Podgląd" na mobile otwiera bottom sheet
    • Zakładka "Widoczność" działa bez zmian (AJAX toggle)
    • Tab "Kontakt" i "Social Media" działają bez zmian

Step 4: Commit ewentualnych poprawek po testach

Jeśli znaleziono problemy, napraw i powtórz deploy.


Podsumowanie zmian

Plik Typ zmiany Opis
templates/base.html 1 linia Nowy {% block head_extra %} po </style>
templates/company_edit.html CSS + HTML + JS Layout grid, Quill init, preview panel, mobile sheet

Zero zmian backendowych — routes, models, sanitization bez zmian.