nordabiz/templates/admin/announcements_form.html
Maciej Pienczyn 110d971dca
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: migrate prod docs to OVH VPS + UTC→Warsaw timezone in all templates
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS
(57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash
commands, memory files, architecture docs, and deploy procedures.

Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted
155 .strftime() calls across 71 templates so timestamps display
in Polish timezone regardless of server timezone.

Also includes: created_by_id tracking, abort import fix, ICS
calendar fix for missing end times, Pros Poland data cleanup.

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

340 lines
12 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}{% if announcement %}Edytuj ogloszenie{% else %}Nowe ogloszenie{% endif %} - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.form-section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
max-width: 900px;
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-xs);
font-weight: 500;
color: var(--text-primary);
}
.form-group label .required {
color: var(--error);
}
.form-group input[type="text"],
.form-group input[type="url"],
.form-group input[type="datetime-local"],
.form-group select,
.form-group textarea {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: var(--font-size-base);
font-family: inherit;
}
.form-group textarea {
min-height: 120px;
resize: vertical;
}
.form-group textarea.content-editor {
min-height: 300px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: var(--font-size-sm);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}
.checkbox-group {
display: flex;
gap: var(--spacing-lg);
flex-wrap: wrap;
}
.checkbox-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
}
.form-hint {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.btn-group {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-xl);
flex-wrap: wrap;
}
.char-counter {
font-size: var(--font-size-xs);
color: var(--text-secondary);
text-align: right;
margin-top: var(--spacing-xs);
}
.char-counter.warning {
color: var(--warning);
}
.char-counter.error {
color: var(--error);
}
.status-info {
background: var(--background);
padding: var(--spacing-md);
border-radius: var(--radius);
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-sm);
}
.status-info strong {
color: var(--primary);
}
.preview-image {
max-width: 200px;
max-height: 150px;
border-radius: var(--radius);
margin-top: var(--spacing-sm);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-xs);
border-bottom: 1px solid var(--border);
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-header">
<h1>{% if announcement %}Edytuj ogloszenie{% else %}Nowe ogloszenie{% endif %}</h1>
{% if announcement %}
<a href="{{ url_for('announcement_detail', slug=announcement.slug) }}" class="btn btn-secondary" target="_blank">
Podglad &#8599;
</a>
{% endif %}
</div>
{% if announcement %}
<div class="status-info">
<strong>Status:</strong> {{ announcement.status_label }}
{% if announcement.published_at %}
| <strong>Opublikowano:</strong> {{ announcement.published_at|local_time('%Y-%m-%d %H:%M') }}
{% endif %}
{% if announcement.views_count %}
| <strong>Wyswietlenia:</strong> {{ announcement.views_count }}
{% endif %}
</div>
{% endif %}
<div class="form-section">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- Basic Info -->
<div class="form-group">
<label for="title">Tytul <span class="required">*</span></label>
<input type="text" id="title" name="title" required maxlength="300"
value="{{ announcement.title if announcement else '' }}"
placeholder="np. Baza noclegowa dla pracownikow budowy elektrowni jadrowej">
</div>
<div class="form-group">
<label for="excerpt">Krotki opis (do listy)</label>
<textarea id="excerpt" name="excerpt" maxlength="500"
placeholder="Krotki opis wyswietlany na liscie ogloszen (max 500 znakow)">{{ announcement.excerpt if announcement else '' }}</textarea>
<div class="char-counter" id="excerpt-counter">0 / 500</div>
</div>
<div class="form-group">
<label for="content">Tresc <span class="required">*</span></label>
<textarea id="content" name="content" required class="content-editor"
placeholder="Pelna tresc ogloszenia (mozesz uzyc HTML)">{{ announcement.content if announcement else '' }}</textarea>
<p class="form-hint">Mozesz uzyc HTML: &lt;p&gt;, &lt;h3&gt;, &lt;ul&gt;, &lt;li&gt;, &lt;a href=""&gt;, &lt;strong&gt;</p>
</div>
<!-- Categorization -->
<h3 class="section-title">Kategoryzacja</h3>
<div class="form-group">
<label>Kategorie <span class="required">*</span></label>
<p class="form-hint" style="margin-bottom: var(--spacing-sm);">Wybierz co najmniej jedną kategorię (możesz wybrać kilka)</p>
<div class="checkbox-group">
{% for cat in categories %}
<label class="checkbox-item">
<input type="checkbox" name="categories" value="{{ cat }}"
{% if announcement and announcement.has_category(cat) %}checked{% endif %}>
<span>{{ category_labels.get(cat, cat) }}</span>
</label>
{% endfor %}
</div>
</div>
<!-- Media -->
<h3 class="section-title">Media i linki</h3>
<div class="form-row">
<div class="form-group">
<label for="image_url">URL obrazka</label>
<input type="url" id="image_url" name="image_url"
value="{{ announcement.image_url if announcement else '' }}"
placeholder="https://example.com/image.jpg">
<p class="form-hint">Opcjonalny obrazek wyswietlany przy ogloszeniu</p>
{% if announcement and announcement.image_url %}
<img src="{{ announcement.image_url }}" alt="Preview" class="preview-image" onerror="this.style.display='none'">
{% endif %}
</div>
<div class="form-group">
<label for="external_link">Link zewnetrzny</label>
<input type="url" id="external_link" name="external_link"
value="{{ announcement.external_link if announcement else '' }}"
placeholder="https://example.com/wiecej-informacji">
<p class="form-hint">Link do zewnetrznego zrodla lub formularza</p>
</div>
</div>
<!-- Publication -->
<h3 class="section-title">Publikacja</h3>
<div class="form-row">
<div class="form-group">
<label for="expires_at">Data wygasniecia</label>
<input type="datetime-local" id="expires_at" name="expires_at"
value="{{ announcement.expires_at|local_time('%Y-%m-%dT%H:%M') if announcement and announcement.expires_at else '' }}">
<p class="form-hint">Pozostaw puste aby nie wygasalo</p>
</div>
</div>
<div class="form-group">
<label>Opcje wyswietlania</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" name="is_pinned"
{% if announcement and announcement.is_pinned %}checked{% endif %}>
<span>&#128204; Przypiete (wyswietlane na gorze)</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="is_featured"
{% if announcement and announcement.is_featured %}checked{% endif %}>
<span>&#11088; Wyrozone (specjalne wyroznienie)</span>
</label>
</div>
</div>
<!-- Actions -->
<div class="btn-group">
<a href="{{ url_for('admin.admin_announcements') }}" class="btn btn-secondary">Anuluj</a>
{% if announcement %}
<!-- Edit mode -->
<button type="submit" name="action" value="save" class="btn btn-primary">
Zapisz zmiany
</button>
{% if announcement.status == 'draft' %}
<button type="submit" name="action" value="publish" class="btn btn-success">
Opublikuj
</button>
{% elif announcement.status == 'published' %}
<button type="submit" name="action" value="archive" class="btn btn-warning">
Archiwizuj
</button>
{% elif announcement.status == 'archived' %}
<button type="submit" name="action" value="publish" class="btn btn-success">
Przywroc i opublikuj
</button>
{% endif %}
{% else %}
<!-- New mode -->
<button type="submit" name="action" value="draft" class="btn btn-secondary">
Zapisz szkic
</button>
<button type="submit" name="action" value="publish" class="btn btn-success">
Opublikuj
</button>
{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
// Character counter for excerpt
const excerptTextarea = document.getElementById('excerpt');
const excerptCounter = document.getElementById('excerpt-counter');
function updateExcerptCounter() {
const length = excerptTextarea.value.length;
excerptCounter.textContent = length + ' / 500';
excerptCounter.classList.remove('warning', 'error');
if (length > 450) {
excerptCounter.classList.add('warning');
}
if (length >= 500) {
excerptCounter.classList.add('error');
}
}
excerptTextarea.addEventListener('input', updateExcerptCounter);
updateExcerptCounter();
// Image preview on URL change
const imageUrlInput = document.getElementById('image_url');
imageUrlInput.addEventListener('change', function() {
const existingPreview = document.querySelector('.preview-image');
if (existingPreview) {
existingPreview.src = this.value;
existingPreview.style.display = this.value ? 'block' : 'none';
} else if (this.value) {
const img = document.createElement('img');
img.src = this.value;
img.alt = 'Preview';
img.className = 'preview-image';
img.onerror = function() { this.style.display = 'none'; };
this.parentNode.appendChild(img);
}
});
{% endblock %}