Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Use stopImmediatePropagation + capture phase to prevent Quill's built-in clipboard handler from also inserting the image as base64. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
712 lines
26 KiB
HTML
Executable File
712 lines
26 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}Nowa wiadomosc - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/quill.snow.css') }}">
|
|
<script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.quill-container {
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
}
|
|
.quill-container .ql-toolbar {
|
|
border-top-left-radius: var(--radius);
|
|
border-top-right-radius: var(--radius);
|
|
}
|
|
.quill-container .ql-container {
|
|
border-bottom-left-radius: var(--radius);
|
|
border-bottom-right-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
}
|
|
.quill-container .ql-editor {
|
|
min-height: 200px;
|
|
}
|
|
.quill-container .ql-editor img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: var(--radius);
|
|
margin: var(--spacing-sm) 0;
|
|
}
|
|
.compose-container {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.compose-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.compose-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.compose-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-xs);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus,
|
|
.form-group textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.recipient-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
padding: var(--spacing-md);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.recipient-avatar {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
.recipient-name {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.recipient-email {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.context-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
padding: var(--spacing-md);
|
|
background: #eff6ff;
|
|
border: 1px solid #bfdbfe;
|
|
border-radius: var(--radius);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.context-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: var(--radius);
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.context-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.context-title {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.context-title a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.context-title a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.recipient-autocomplete {
|
|
position: relative;
|
|
}
|
|
|
|
.recipient-autocomplete input[type="text"] {
|
|
width: 100%;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.recipient-autocomplete input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.autocomplete-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
border-radius: 0 0 var(--radius) var(--radius);
|
|
max-height: 240px;
|
|
overflow-y: auto;
|
|
z-index: 100;
|
|
box-shadow: var(--shadow-lg);
|
|
display: none;
|
|
}
|
|
|
|
.autocomplete-results.visible {
|
|
display: block;
|
|
}
|
|
|
|
.autocomplete-item {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.autocomplete-item:hover,
|
|
.autocomplete-item.active {
|
|
background: var(--background);
|
|
}
|
|
|
|
.autocomplete-item-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.autocomplete-item-name {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.autocomplete-item-email {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.autocomplete-no-results {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.selected-recipient {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: var(--background);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.selected-recipient-remove {
|
|
margin-left: auto;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
padding: 4px;
|
|
border-radius: var(--radius-sm);
|
|
line-height: 1;
|
|
}
|
|
|
|
.selected-recipient-remove:hover {
|
|
color: var(--danger);
|
|
background: rgba(239, 68, 68, 0.1);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="compose-container">
|
|
<div style="display: flex; align-items: center; gap: var(--spacing-lg); flex-wrap: wrap; margin-bottom: var(--spacing-lg);">
|
|
<a href="{{ url_for('messages_inbox') }}" class="back-link" style="margin-bottom: 0;">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
</svg>
|
|
Powrot do wiadomosci
|
|
</a>
|
|
{% if from_company %}
|
|
<a href="{{ url_for('company_detail', company_id=from_company.id) }}" class="back-link" style="margin-bottom: 0;">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
</svg>
|
|
Powrot do {{ from_company.name }}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="compose-header">
|
|
<h1>Nowa wiadomosc</h1>
|
|
</div>
|
|
|
|
<div class="compose-card">
|
|
{% if recipient %}
|
|
<div class="recipient-info">
|
|
<div class="recipient-avatar">{{ (recipient.name or recipient.email)[0].upper() }}</div>
|
|
<div>
|
|
<div class="recipient-name">{{ recipient.name or recipient.email.split('@')[0] }}</div>
|
|
{% if recipient.privacy_show_email != False %}
|
|
<div class="recipient-email">{{ recipient.email }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if context %}
|
|
<div class="context-info">
|
|
<div class="context-icon">
|
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="context-label">Dotyczy ogloszenia B2B:</div>
|
|
<div class="context-title"><a href="{{ context.url }}" target="_blank">{{ context.title }}</a></div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" action="{{ url_for('messages_send') }}" enctype="multipart/form-data">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
{% if context_type %}
|
|
<input type="hidden" name="context_type" value="{{ context_type }}">
|
|
<input type="hidden" name="context_id" value="{{ context_id }}">
|
|
{% endif %}
|
|
|
|
{% if recipient %}
|
|
<input type="hidden" name="recipient_id" value="{{ recipient.id }}">
|
|
{% else %}
|
|
<div class="form-group">
|
|
<label for="recipient_search">Do *</label>
|
|
<input type="hidden" id="recipient_id" name="recipient_id" required>
|
|
<div id="recipient-selected" class="selected-recipient" style="display: none;">
|
|
<div class="autocomplete-item-avatar" id="selected-avatar"></div>
|
|
<div>
|
|
<div class="autocomplete-item-name" id="selected-name"></div>
|
|
<div class="autocomplete-item-email" id="selected-email"></div>
|
|
</div>
|
|
<button type="button" class="selected-recipient-remove" onclick="clearRecipient()" title="Zmien odbiorcę">✕</button>
|
|
</div>
|
|
<div id="recipient-preview" style="display: none; margin-top: 8px; padding: 12px 16px; background: var(--bg-secondary); border-radius: var(--radius); border: 1px solid var(--border-color);">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<div id="preview-avatar" style="width: 40px; height: 40px; border-radius: 50%; background: var(--primary); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 16px;"></div>
|
|
<div>
|
|
<div id="preview-name" style="font-weight: 600; color: var(--text-primary);"></div>
|
|
<div id="preview-company" style="font-size: var(--font-size-sm); color: var(--text-secondary);"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="recipient-autocomplete" class="recipient-autocomplete">
|
|
<input type="text" id="recipient_search" placeholder="Wpisz imie, nazwisko lub email..." autocomplete="off">
|
|
<div id="autocomplete-results" class="autocomplete-results"></div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="form-group">
|
|
<label for="subject">Temat</label>
|
|
<input type="text" id="subject" name="subject" maxlength="255" placeholder="Temat wiadomosci (opcjonalnie)" value="{{ context_subject or '' }}">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Treść *</label>
|
|
<div id="quill-content" class="quill-container" style="min-height: 200px; background: var(--surface);"></div>
|
|
<textarea id="content" name="content" style="display:none;" required></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Załączniki (maks. 3 pliki, 15MB łącznie)</label>
|
|
<div class="file-upload-zone" id="file-drop-zone" style="border: 2px dashed var(--border-color); border-radius: var(--radius); padding: 20px; text-align: center; cursor: pointer; transition: border-color 0.2s;">
|
|
<input type="file" name="attachments" id="file-input" multiple accept=".jpg,.jpeg,.png,.gif,.pdf,.docx,.xlsx" style="display: none;">
|
|
<p style="margin: 0; color: var(--text-secondary); font-size: var(--font-size-sm);">
|
|
Przeciągnij pliki tutaj lub <a href="#" onclick="document.getElementById('file-input').click(); return false;" style="color: var(--primary);">wybierz z dysku</a>
|
|
</p>
|
|
<p style="margin: 4px 0 0; color: var(--text-secondary); font-size: var(--font-size-xs);">
|
|
JPG, PNG, GIF (5MB) · PDF, DOCX, XLSX (10MB)
|
|
</p>
|
|
</div>
|
|
<div id="file-list" style="margin-top: 8px;"></div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">Wyslij</button>
|
|
<a href="{{ url_for('messages_inbox') }}" class="btn btn-secondary">Anuluj</a>
|
|
{% if recipient and recipient.email and recipient.privacy_show_email != False %}
|
|
<a href="mailto:{{ recipient.email }}" class="btn btn-secondary" style="margin-left: auto; display: inline-flex; align-items: center; gap: 6px;">
|
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
|
|
Wyslij e-mail
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="form-hint" style="text-align: center; margin-top: 12px; padding: 8px 16px; font-size: var(--font-size-sm); color: var(--text-secondary);">
|
|
📧 Odbiorca zostanie powiadomiony o nowej wiadomości emailem
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
{% if not recipient %}
|
|
(function() {
|
|
var users = [
|
|
{% for user in users %}
|
|
{id: {{ user.id }}, name: {{ (user.name or user.email.split('@')[0]) | tojson }}, email: {{ user.email | tojson }}, showEmail: {{ 'true' if user.privacy_show_email != False else 'false' }}, companyName: {{ (user._company_name or '') | tojson }}, companySlug: {{ (user._company_slug or '') | tojson }}, position: {{ (user._position or '') | tojson }}}{{ ',' if not loop.last }}
|
|
{% endfor %}
|
|
];
|
|
|
|
var searchInput = document.getElementById('recipient_search');
|
|
var resultsDiv = document.getElementById('autocomplete-results');
|
|
var hiddenInput = document.getElementById('recipient_id');
|
|
var selectedDiv = document.getElementById('recipient-selected');
|
|
var autocompleteDiv = document.getElementById('recipient-autocomplete');
|
|
var activeIndex = -1;
|
|
|
|
function normalize(str) {
|
|
return str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
}
|
|
|
|
function filterUsers(query) {
|
|
if (!query) return [];
|
|
var q = normalize(query);
|
|
return users.filter(function(u) {
|
|
return normalize(u.name).indexOf(q) !== -1 || normalize(u.email).indexOf(q) !== -1;
|
|
});
|
|
}
|
|
|
|
function renderResults(matches) {
|
|
activeIndex = -1;
|
|
if (matches.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="autocomplete-no-results">Nie znaleziono</div>';
|
|
resultsDiv.classList.add('visible');
|
|
return;
|
|
}
|
|
resultsDiv.innerHTML = matches.map(function(u, i) {
|
|
var initial = (u.name || u.email)[0].toUpperCase();
|
|
var emailPart = u.showEmail ? '<div class="autocomplete-item-email">' + u.email + '</div>' : '';
|
|
return '<div class="autocomplete-item" data-index="' + i + '" data-id="' + u.id + '" data-name="' + u.name.replace(/"/g, '"') + '" data-email="' + u.email.replace(/"/g, '"') + '" data-show-email="' + u.showEmail + '">' +
|
|
'<div class="autocomplete-item-avatar">' + initial + '</div>' +
|
|
'<div><div class="autocomplete-item-name">' + u.name + '</div>' + emailPart + '</div></div>';
|
|
}).join('');
|
|
resultsDiv.classList.add('visible');
|
|
}
|
|
|
|
function selectRecipient(id, name, email, showEmail) {
|
|
hiddenInput.value = id;
|
|
document.getElementById('selected-avatar').textContent = (name || email)[0].toUpperCase();
|
|
document.getElementById('selected-name').textContent = name;
|
|
document.getElementById('selected-email').textContent = showEmail ? email : '';
|
|
selectedDiv.style.display = 'flex';
|
|
autocompleteDiv.style.display = 'none';
|
|
resultsDiv.classList.remove('visible');
|
|
searchInput.value = '';
|
|
// Show recipient preview card
|
|
var user = users.find(function(u) { return u.id === id; });
|
|
var previewDiv = document.getElementById('recipient-preview');
|
|
if (user && (user.companyName)) {
|
|
document.getElementById('preview-avatar').textContent = (name || email)[0].toUpperCase();
|
|
document.getElementById('preview-name').textContent = name;
|
|
var companyHtml = '';
|
|
if (user.companyName) {
|
|
companyHtml = user.companySlug
|
|
? '<a href="/firma/' + user.companySlug + '" target="_blank" style="color: var(--primary); text-decoration: none;">' + user.companyName + '</a>'
|
|
: user.companyName;
|
|
}
|
|
if (user.position) {
|
|
companyHtml = user.position + (companyHtml ? ' · ' + companyHtml : '');
|
|
}
|
|
document.getElementById('preview-company').innerHTML = companyHtml;
|
|
previewDiv.style.display = 'block';
|
|
} else {
|
|
previewDiv.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
window.clearRecipient = function() {
|
|
hiddenInput.value = '';
|
|
selectedDiv.style.display = 'none';
|
|
autocompleteDiv.style.display = 'block';
|
|
searchInput.value = '';
|
|
searchInput.focus();
|
|
document.getElementById('recipient-preview').style.display = 'none';
|
|
};
|
|
|
|
searchInput.addEventListener('input', function() {
|
|
var q = this.value.trim();
|
|
if (q.length === 0) {
|
|
resultsDiv.classList.remove('visible');
|
|
return;
|
|
}
|
|
renderResults(filterUsers(q));
|
|
});
|
|
|
|
searchInput.addEventListener('focus', function() {
|
|
if (this.value.trim().length > 0) {
|
|
renderResults(filterUsers(this.value.trim()));
|
|
}
|
|
});
|
|
|
|
resultsDiv.addEventListener('click', function(e) {
|
|
var item = e.target.closest('.autocomplete-item');
|
|
if (item) {
|
|
selectRecipient(item.dataset.id, item.dataset.name, item.dataset.email, item.dataset.showEmail === 'true');
|
|
}
|
|
});
|
|
|
|
searchInput.addEventListener('keydown', function(e) {
|
|
var items = resultsDiv.querySelectorAll('.autocomplete-item');
|
|
if (!items.length) return;
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
activeIndex = Math.min(activeIndex + 1, items.length - 1);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
activeIndex = Math.max(activeIndex - 1, 0);
|
|
} else if (e.key === 'Enter' && activeIndex >= 0) {
|
|
e.preventDefault();
|
|
var item = items[activeIndex];
|
|
selectRecipient(item.dataset.id, item.dataset.name, item.dataset.email, item.dataset.showEmail === 'true');
|
|
return;
|
|
} else if (e.key === 'Escape') {
|
|
resultsDiv.classList.remove('visible');
|
|
return;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
items.forEach(function(el) { el.classList.remove('active'); });
|
|
items[activeIndex].classList.add('active');
|
|
items[activeIndex].scrollIntoView({block: 'nearest'});
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!autocompleteDiv.contains(e.target)) {
|
|
resultsDiv.classList.remove('visible');
|
|
}
|
|
});
|
|
|
|
searchInput.closest('form').addEventListener('submit', function(e) {
|
|
if (!hiddenInput.value) {
|
|
e.preventDefault();
|
|
searchInput.focus();
|
|
searchInput.style.borderColor = 'var(--danger)';
|
|
setTimeout(function() { searchInput.style.borderColor = ''; }, 2000);
|
|
}
|
|
});
|
|
})();
|
|
{% endif %}
|
|
|
|
// File attachment handling
|
|
(function() {
|
|
var dropZone = document.getElementById('file-drop-zone');
|
|
var fileInput = document.getElementById('file-input');
|
|
var fileList = document.getElementById('file-list');
|
|
if (!dropZone) return;
|
|
|
|
dropZone.addEventListener('click', function(e) {
|
|
if (e.target.tagName !== 'A') fileInput.click();
|
|
});
|
|
|
|
dropZone.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
dropZone.style.borderColor = 'var(--primary)';
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', function() {
|
|
dropZone.style.borderColor = 'var(--border-color)';
|
|
});
|
|
|
|
dropZone.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
dropZone.style.borderColor = 'var(--border-color)';
|
|
var dt = new DataTransfer();
|
|
Array.from(e.dataTransfer.files).forEach(function(f) { dt.items.add(f); });
|
|
Array.from(fileInput.files).forEach(function(f) { dt.items.add(f); });
|
|
fileInput.files = dt.files;
|
|
updateFileList();
|
|
});
|
|
|
|
fileInput.addEventListener('change', updateFileList);
|
|
|
|
function updateFileList() {
|
|
var files = Array.from(fileInput.files);
|
|
if (files.length === 0) {
|
|
fileList.innerHTML = '';
|
|
return;
|
|
}
|
|
fileList.innerHTML = files.map(function(f, i) {
|
|
var sizeMB = (f.size / 1024 / 1024).toFixed(1);
|
|
return '<div style="display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: var(--font-size-sm);">' +
|
|
'<span style="color: var(--text-secondary);">📎</span> ' +
|
|
'<span>' + f.name + '</span> ' +
|
|
'<span style="color: var(--text-secondary);">(' + sizeMB + ' MB)</span> ' +
|
|
'<a href="#" onclick="removeFile(' + i + '); return false;" style="color: var(--danger); margin-left: auto;">✕</a>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
window.removeFile = function(index) {
|
|
var dt = new DataTransfer();
|
|
Array.from(fileInput.files).forEach(function(f, i) {
|
|
if (i !== index) dt.items.add(f);
|
|
});
|
|
fileInput.files = dt.files;
|
|
updateFileList();
|
|
};
|
|
})();
|
|
|
|
/* Quill editor for message content */
|
|
(function() {
|
|
var csrfToken = '{{ csrf_token() }}';
|
|
var quill = new Quill('#quill-content', {
|
|
theme: 'snow',
|
|
placeholder: 'Napisz wiadomość...',
|
|
modules: {
|
|
toolbar: {
|
|
container: [
|
|
['bold', 'italic'],
|
|
[{'list': 'ordered'}, {'list': 'bullet'}],
|
|
['link', 'image'],
|
|
['clean']
|
|
],
|
|
handlers: {
|
|
image: function() {
|
|
var input = document.createElement('input');
|
|
input.setAttribute('type', 'file');
|
|
input.setAttribute('accept', 'image/*');
|
|
input.click();
|
|
input.onchange = function() {
|
|
if (input.files && input.files[0]) {
|
|
uploadImage(input.files[0]);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
function uploadImage(file) {
|
|
var fd = new FormData();
|
|
fd.append('image', file);
|
|
fetch('/api/messages/upload-image', {
|
|
method: 'POST',
|
|
headers: {'X-CSRFToken': csrfToken},
|
|
body: fd
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.url) {
|
|
var range = quill.getSelection(true);
|
|
quill.insertEmbed(range.index, 'image', data.url);
|
|
quill.setSelection(range.index + 1);
|
|
} else {
|
|
alert(data.error || 'Błąd uploadu');
|
|
}
|
|
})
|
|
.catch(function() { alert('Błąd połączenia'); });
|
|
}
|
|
|
|
/* Handle paste with images (screenshots) — capture phase to beat Quill's handler */
|
|
quill.root.addEventListener('paste', function(e) {
|
|
var items = (e.clipboardData || {}).items || [];
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (items[i].type.indexOf('image') !== -1) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
var file = items[i].getAsFile();
|
|
if (file) uploadImage(file);
|
|
return;
|
|
}
|
|
}
|
|
}, true);
|
|
|
|
/* Sync Quill content to hidden textarea on every change */
|
|
var textarea = document.getElementById('content');
|
|
quill.on('text-change', function() {
|
|
var html = quill.root.innerHTML;
|
|
textarea.value = (html === '<p><br></p>') ? '' : html;
|
|
});
|
|
|
|
/* Validate before submit */
|
|
document.querySelector('form').addEventListener('submit', function(e) {
|
|
var html = quill.root.innerHTML;
|
|
textarea.value = (html === '<p><br></p>') ? '' : html;
|
|
if (!textarea.value.trim()) {
|
|
e.preventDefault();
|
|
alert('Treść wiadomości jest wymagana.');
|
|
}
|
|
});
|
|
})();
|
|
{% endblock %}
|