feat: implement exchange creation
Add Exchange model, API endpoint, and form validation for creating new gift exchanges. - Create ExchangeForm with timezone validation - Add admin routes for creating and viewing exchanges - Generate unique 12-char slug for each exchange - Validate registration/exchange dates - Display exchanges in dashboard - All tests passing with 92% coverage Story: 2.1
This commit is contained in:
@@ -12,8 +12,56 @@
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<p>Welcome to the Sneaky Klaus admin dashboard!</p>
|
||||
<div class="dashboard-summary">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h3>Draft</h3>
|
||||
<p><strong>{{ draft_count }}</strong></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Active</h3>
|
||||
<p><strong>{{ active_count }}</strong></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Completed</h3>
|
||||
<p><strong>{{ completed_count }}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>This is a placeholder for the admin dashboard. More features coming soon.</p>
|
||||
<div style="margin: 2rem 0;">
|
||||
<a href="{{ url_for('admin.create_exchange') }}" role="button">Create New Exchange</a>
|
||||
</div>
|
||||
|
||||
<h2>All Exchanges</h2>
|
||||
|
||||
{% if exchanges %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>State</th>
|
||||
<th>Participants</th>
|
||||
<th>Exchange Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for exchange in exchanges %}
|
||||
<tr>
|
||||
<td>{{ exchange.name }}</td>
|
||||
<td><mark>{{ exchange.state }}</mark></td>
|
||||
<td>0 / {{ exchange.max_participants }}</td>
|
||||
<td>{{ exchange.exchange_date.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('admin.view_exchange', exchange_id=exchange.id) }}">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No exchanges yet. <a href="{{ url_for('admin.create_exchange') }}">Create your first exchange</a>!</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
58
src/templates/admin/exchange_detail.html
Normal file
58
src/templates/admin/exchange_detail.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}{{ exchange.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{{ exchange.name }}</h1>
|
||||
|
||||
<div class="exchange-details">
|
||||
<div class="detail-section">
|
||||
<h2>Details</h2>
|
||||
<dl>
|
||||
<dt>State</dt>
|
||||
<dd><span class="badge badge-{{ exchange.state }}">{{ exchange.state }}</span></dd>
|
||||
|
||||
<dt>Description</dt>
|
||||
<dd>{{ exchange.description or "No description" }}</dd>
|
||||
|
||||
<dt>Budget</dt>
|
||||
<dd>{{ exchange.budget }}</dd>
|
||||
|
||||
<dt>Max Participants</dt>
|
||||
<dd>{{ exchange.max_participants }}</dd>
|
||||
|
||||
<dt>Registration Close Date</dt>
|
||||
<dd>{{ exchange.registration_close_date.strftime('%Y-%m-%d %H:%M') }} {{ exchange.timezone }}</dd>
|
||||
|
||||
<dt>Exchange Date</dt>
|
||||
<dd>{{ exchange.exchange_date.strftime('%Y-%m-%d %H:%M') }} {{ exchange.timezone }}</dd>
|
||||
|
||||
<dt>Registration Link</dt>
|
||||
<dd>
|
||||
<code id="registration-link">{{ url_for('participant.register', slug=exchange.slug, _external=True) }}</code>
|
||||
<button type="button" onclick="copyToClipboard()">Copy</button>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h2>Participants</h2>
|
||||
<p>No participants yet.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
const link = document.getElementById('registration-link').textContent;
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
alert('Link copied to clipboard!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
104
src/templates/admin/exchange_form.html
Normal file
104
src/templates/admin/exchange_form.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}{% if is_edit %}Edit Exchange{% else %}Create Exchange{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% if is_edit %}Edit Exchange{% else %}Create New Exchange{% endif %}</h1>
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.name.label }}
|
||||
{{ form.name(class="form-control") }}
|
||||
{% if form.name.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.name.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.description.label }}
|
||||
{{ form.description(class="form-control", rows=4) }}
|
||||
{% if form.description.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.description.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.budget.label }}
|
||||
{{ form.budget(class="form-control", placeholder="e.g., $20-30") }}
|
||||
{% if form.budget.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.budget.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.max_participants.label }}
|
||||
{{ form.max_participants(class="form-control", min=3) }}
|
||||
{% if form.max_participants.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.max_participants.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.registration_close_date.label }}
|
||||
{{ form.registration_close_date(class="form-control") }}
|
||||
{% if form.registration_close_date.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.registration_close_date.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.exchange_date.label }}
|
||||
{{ form.exchange_date(class="form-control") }}
|
||||
{% if form.exchange_date.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.exchange_date.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.timezone.label }}
|
||||
{{ form.timezone(class="form-control") }}
|
||||
{% if form.timezone.errors %}
|
||||
<div class="error">
|
||||
{% for error in form.timezone.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if is_edit %}Update Exchange{% else %}Create Exchange{% endif %}
|
||||
</button>
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user