nordabiz/templates/forum/new_topic.html
Maciej Pienczyn dc6c711264
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
feat(forum): @mention autocomplete with user picker
- New reusable component static/js/mention-autocomplete.js
- @ button above reply textarea + new topic textarea
- Typing @xxx after space triggers dropdown with avatar/name/company
- Arrow keys navigate, Enter/Tab selects, Esc closes
- Inserts @firstname.lastname (matches existing backend mention parser)
- Uses existing GET /api/users/search endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:38:09 +02:00

513 lines
16 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Nowy temat - Forum - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.new-topic-container {
max-width: 800px;
margin: 0 auto;
}
.topic-breadcrumb {
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.topic-breadcrumb a {
color: var(--primary);
text-decoration: none;
}
.topic-breadcrumb a:hover {
text-decoration: underline;
}
.new-topic-form {
background: var(--surface);
border-radius: var(--radius-xl);
padding: var(--spacing-2xl);
box-shadow: var(--shadow-lg);
}
.form-header {
margin-bottom: var(--spacing-xl);
}
.form-header h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.form-header p {
color: var(--text-secondary);
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-label {
display: block;
font-weight: 500;
margin-bottom: var(--spacing-sm);
color: var(--text-primary);
}
.form-label .required {
color: var(--error);
}
.form-input, .form-select {
width: 100%;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
font-family: var(--font-family);
transition: var(--transition);
background: var(--surface);
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-textarea {
min-height: 200px;
resize: vertical;
}
.form-hint {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.form-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-xl);
}
.guidelines {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: var(--radius);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.guidelines h3 {
font-size: var(--font-size-base);
font-weight: 600;
color: #0369a1;
margin-bottom: var(--spacing-sm);
}
.guidelines ul {
margin: 0;
padding-left: var(--spacing-lg);
color: #0c4a6e;
font-size: var(--font-size-sm);
}
.guidelines li {
margin-bottom: var(--spacing-xs);
}
/* Category select styles */
.category-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
/* Upload dropzone */
.upload-dropzone {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: var(--spacing-xl);
text-align: center;
background: var(--background);
transition: var(--transition);
cursor: pointer;
}
.upload-dropzone:hover, .upload-dropzone.drag-over {
border-color: var(--primary);
background: rgba(37, 99, 235, 0.05);
}
.upload-dropzone svg {
width: 48px;
height: 48px;
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
.upload-dropzone p {
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.upload-dropzone .upload-hint {
font-size: var(--font-size-sm);
color: var(--text-tertiary);
}
.upload-preview {
display: none;
margin-top: var(--spacing-md);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
border: 1px solid var(--border);
}
.upload-preview.active {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.upload-preview img {
max-width: 120px;
max-height: 80px;
border-radius: var(--radius-sm);
object-fit: cover;
}
.upload-preview .file-info {
flex: 1;
}
.upload-preview .file-name {
font-weight: 500;
color: var(--text-primary);
}
.upload-preview .file-size {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.upload-preview .remove-file {
color: var(--error);
cursor: pointer;
padding: var(--spacing-sm);
}
.upload-preview .remove-file:hover {
background: rgba(239, 68, 68, 0.1);
border-radius: var(--radius);
}
@media (max-width: 768px) {
.new-topic-form {
padding: var(--spacing-lg);
}
.category-group {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.form-actions .btn {
width: 100%;
}
}
</style>
{% endblock %}
{% block content %}
<div class="new-topic-container">
<nav class="topic-breadcrumb">
<a href="{{ url_for('forum_index') }}">Forum</a> &raquo; Nowy temat
</nav>
<div class="new-topic-form">
<div class="form-header">
<h1>Utwórz nowy temat</h1>
<p>Rozpocznij dyskusję z innymi członkami Norda Biznes</p>
</div>
<div class="guidelines">
<h3>Zasady forum</h3>
<ul>
<li>Pisz zwięźle i na temat</li>
<li>Szanuj innych członków</li>
<li>Nie publikuj reklam ani spamu</li>
<li>Unikaj poufnych informacji biznesowych</li>
</ul>
</div>
<form method="POST" action="{{ url_for('forum_new_topic') }}" enctype="multipart/form-data" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="category-group">
<div class="form-group">
<label for="category" class="form-label">
Kategoria <span class="required">*</span>
</label>
<select id="category" name="category" class="form-select" required>
{% for cat in categories %}
<option value="{{ cat }}" {% if cat == 'question' %}selected{% endif %}>
{{ category_labels.get(cat, cat) }}
</option>
{% endfor %}
</select>
<p class="form-hint">Wybierz typ tematu</p>
</div>
<div class="form-group">
<label for="title" class="form-label">
Tytul tematu <span class="required">*</span>
</label>
<input
type="text"
id="title"
name="title"
class="form-input"
placeholder="Krótki, opisowy tytuł..."
required
maxlength="255"
minlength="5"
autofocus
>
<p class="form-hint">Minimum 5 znaków</p>
</div>
</div>
<div class="form-group">
<label for="content" class="form-label">
Treść <span class="required">*</span>
</label>
<div style="display:flex;gap:6px;margin-bottom:6px;">
<button type="button" class="btn btn-outline btn-sm" id="mentionBtnNew" title="Wspomnij użytkownika (@)" style="padding:4px 10px;font-weight:600;">@</button>
</div>
<textarea
id="content"
name="content"
class="form-input form-textarea"
placeholder="Opisz temat, zadaj pytanie lub podziel się informacją..."
required
minlength="10"
></textarea>
<p class="form-hint">Minimum 10 znaków. Wpisz @ aby wspomnieć użytkownika.</p>
</div>
<div class="form-group">
<label class="form-label">
Załącznik (opcjonalnie)
</label>
<div class="upload-dropzone" id="dropzone">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p>Przeciągnij obraz lub kliknij tutaj</p>
<span class="upload-hint">Możesz też wkleić ze schowka (Ctrl+V)</span>
<span class="upload-hint">JPG, PNG, GIF do 5MB</span>
<input type="file" id="attachment" name="attachment" accept="image/jpeg,image/png,image/gif" style="display: none;">
</div>
<div class="upload-preview" id="uploadPreview">
<img id="previewImage" src="" alt="Preview">
<div class="file-info">
<div class="file-name" id="fileName"></div>
<div class="file-size" id="fileSize"></div>
</div>
<div class="remove-file" id="removeFile" title="Usuń">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary btn-lg">
Utwórz temat
</button>
<a href="{{ url_for('forum_index') }}" class="btn btn-outline btn-lg">
Anuluj
</a>
</div>
</form>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
.toast.warning { border-left-color: var(--warning); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', filename='js/mention-autocomplete.js') }}"></script>
<script>
(function() {
var ta = document.getElementById('content');
var mentionBtn = document.getElementById('mentionBtnNew');
if (ta && window.attachMentionAutocomplete) window.attachMentionAutocomplete(ta);
if (mentionBtn && ta && window.insertMentionTrigger) {
mentionBtn.addEventListener('click', function() { window.insertMentionTrigger(ta); });
}
})();
</script>
{% endblock %}
{% block extra_js %}
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
// Client-side validation
document.querySelector('form').addEventListener('submit', function(e) {
const title = document.getElementById('title');
const content = document.getElementById('content');
let valid = true;
if (title.value.length < 5) {
title.style.borderColor = 'var(--error)';
valid = false;
} else {
title.style.borderColor = '';
}
if (content.value.length < 10) {
content.style.borderColor = 'var(--error)';
valid = false;
} else {
content.style.borderColor = '';
}
if (!valid) {
e.preventDefault();
return;
}
const submitBtn = this.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.dataset.originalText = submitBtn.textContent;
submitBtn.textContent = 'Wysyłanie…';
}
});
// File upload handling
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('attachment');
const uploadPreview = document.getElementById('uploadPreview');
const previewImage = document.getElementById('previewImage');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const removeFile = document.getElementById('removeFile');
// Click to upload
dropzone.addEventListener('click', () => fileInput.click());
// Drag and drop
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drag-over');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
handleFile(file);
}
});
// File input change
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
handleFile(file);
}
});
// Paste from clipboard (Ctrl+V)
document.addEventListener('paste', (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
e.preventDefault();
const file = items[i].getAsFile();
if (file) {
handleFile(file);
}
break;
}
}
});
// Remove file
removeFile.addEventListener('click', () => {
fileInput.value = '';
uploadPreview.classList.remove('active');
dropzone.style.display = 'block';
});
function handleFile(file) {
// Validate file size (5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('Plik jest za duży (max 5MB)', 'error');
return;
}
// Validate file type
if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
showToast('Dozwolone formaty: JPG, PNG, GIF', 'warning');
return;
}
// Create a new File object and assign to input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
// Show preview
const reader = new FileReader();
reader.onload = (e) => {
previewImage.src = e.target.result;
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
uploadPreview.classList.add('active');
dropzone.style.display = 'none';
};
reader.readAsDataURL(file);
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
{% endblock %}