- Add /admin/ai-usage/user/<id> route for detailed AI usage per user - Add ai_usage_user.html template with stats, usage breakdown, logs - Make user names clickable in AI usage dashboard ranking - Replace all native browser dialogs (alert, confirm) with styled modals/toasts: - admin/fees.html, forum.html, recommendations.html, announcements.html, debug.html - calendar/admin.html, event.html - company_detail.html, company/recommend.html - forum/new_topic.html, topic.html - classifieds/view.html - auth/reset_password.html Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
488 lines
15 KiB
HTML
Executable File
488 lines
15 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}Nowy temat - Forum - Norda Biznes Hub{% 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> » Nowy temat
|
||
</nav>
|
||
|
||
<div class="new-topic-form">
|
||
<div class="form-header">
|
||
<h1>Utworz nowy temat</h1>
|
||
<p>Rozpocznij dyskusje z innymi czlonkami Norda Biznes</p>
|
||
</div>
|
||
|
||
<div class="guidelines">
|
||
<h3>Zasady forum</h3>
|
||
<ul>
|
||
<li>Pisz zwiezle i na temat</li>
|
||
<li>Szanuj innych czlonkow</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="Krotki, opisowy tytul..."
|
||
required
|
||
maxlength="255"
|
||
minlength="5"
|
||
autofocus
|
||
>
|
||
<p class="form-hint">Minimum 5 znakow</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="content" class="form-label">
|
||
Tresc <span class="required">*</span>
|
||
</label>
|
||
<textarea
|
||
id="content"
|
||
name="content"
|
||
class="form-input form-textarea"
|
||
placeholder="Opisz temat, zadaj pytanie lub podziel sie informacja..."
|
||
required
|
||
minlength="10"
|
||
></textarea>
|
||
<p class="form-hint">Minimum 10 znakow. Im wiecej szczegolow, tym lepsze odpowiedzi.</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Zalacznik (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>Przeciagnij obraz lub kliknij tutaj</p>
|
||
<span class="upload-hint">Mozesz tez wkleic 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="Usun">
|
||
<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">
|
||
Utworz 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_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();
|
||
}
|
||
});
|
||
|
||
// 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 %}
|