Add password reset flow, attendee profile enhancements, and spec
- Add forgot_password and reset_password templates and routes - Enhance profile.html with photo crop/zoom functionality - Add attendee type management to edit_attendee.html - Update templates styling and layout consistency - Document database schema in SPEC.md - Add CLAUDE.md project guidance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,47 +4,75 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="attendees-page">
|
||||
<h1>{{ 'event_attendees'|t }}</h1>
|
||||
<p>{{ 'connect_with_attendees'|t }}</p>
|
||||
|
||||
{% if attendees %}
|
||||
<div class="attendees-grid">
|
||||
{% for att in attendees %}
|
||||
<div class="attendee-card">
|
||||
<div class="attendee-photo">
|
||||
{% if att.profile_picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + att.profile_picture) }}" alt="Photo">
|
||||
{% else %}
|
||||
<div class="no-photo">{{ att.first_name[0] }}{{ att.last_name[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="attendee-info">
|
||||
<h3>{{ att.first_name }} {{ att.last_name }}</h3>
|
||||
<p class="attendee-org">{{ (att.organisation)|spacify if att.organisation else '' }}</p>
|
||||
<p class="attendee-role">{{ (att.role)|spacify if att.role else '' }}</p>
|
||||
{% if att.introduction %}
|
||||
<p class="attendee-intro">{{ att.introduction[:100] }}{% if att.introduction|length > 100 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="attendee-actions">
|
||||
{% if att.my_status == 'accepted' %}
|
||||
<span class="badge badge-success">{{ 'connected'|t }}</span>
|
||||
{% elif att.my_status == 'pending' %}
|
||||
<span class="badge badge-pending">{{ 'request_pending'|t }}</span>
|
||||
{% elif att.my_status == 'rejected' %}
|
||||
<span class="badge badge-rejected">{{ 'rejected'|t }}</span>
|
||||
{% else %}
|
||||
<form method="POST" action="{{ url_for('create_connection') }}" class="connect-form">
|
||||
<input type="hidden" name="connected_attendee_id" value="{{ att.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-primary">{{ 'connect'|t }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if event %}
|
||||
<div class="event-info-header">
|
||||
<h1>{{ event.name }}</h1>
|
||||
<div class="event-meta">
|
||||
{% if event.location %}
|
||||
<span class="event-location">{{ event.location }}</span>
|
||||
{% endif %}
|
||||
{% if event.start_date %}
|
||||
<span class="event-date">{{ event.start_date|format_date }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="no-attendees">{{ 'no_other_attendees'|t }}</p>
|
||||
{% if event.description %}
|
||||
<p class="event-description">{{ event.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="page-subtitle">{{ 'connect_with_attendees'|t }}</p>
|
||||
|
||||
<div class="attendee-search">
|
||||
<form method="GET" action="{{ url_for('list_attendees') }}">
|
||||
<input type="text" name="q" value="{{ search_query }}" placeholder="{{ 'search_attendees'|t }}">
|
||||
<button type="submit" class="btn btn-primary">{{ 'search'|t }}</button>
|
||||
</form>
|
||||
<p class="search-hint">{{ 'search_min_chars'|t|format(min_chars) }}</p>
|
||||
</div>
|
||||
|
||||
{% if search_query and search_query|length >= min_chars %}
|
||||
{% if too_broad %}
|
||||
<p class="search-too-broad">{{ 'search_too_broad'|t }}</p>
|
||||
{% elif attendees %}
|
||||
<div class="attendees-grid">
|
||||
{% for att in attendees %}
|
||||
<div class="attendee-card">
|
||||
<div class="attendee-photo">
|
||||
{% if att.profile_picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + att.profile_picture) }}" alt="Photo">
|
||||
{% else %}
|
||||
<div class="no-photo">{{ att.first_name[0] }}{{ att.last_name[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="attendee-info">
|
||||
<h3>{{ att.first_name }} {{ att.last_name }}</h3>
|
||||
<p class="attendee-org">{{ att.organisation if att.organisation else '' }}</p>
|
||||
<p class="attendee-role">{{ att.role if att.role else '' }}</p>
|
||||
{% if att.introduction %}
|
||||
<p class="attendee-intro">{{ att.introduction[:100] }}{% if att.introduction|length > 100 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="attendee-actions">
|
||||
{% if att.my_status == 'accepted' %}
|
||||
<span class="badge badge-success">{{ 'connected'|t }}</span>
|
||||
{% elif att.my_status == 'pending' %}
|
||||
<span class="badge badge-pending">{{ 'request_pending'|t }}</span>
|
||||
{% elif att.my_status == 'rejected' %}
|
||||
<span class="badge badge-rejected">{{ 'rejected'|t }}</span>
|
||||
{% else %}
|
||||
<form method="POST" action="{{ url_for('create_connection') }}" class="connect-form">
|
||||
<input type="hidden" name="connected_attendee_id" value="{{ att.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-primary">{{ 'connect'|t }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="no-attendees">{{ 'no_attendees_found'|t }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="request-info">
|
||||
<h3>{{ req.first_name }} {{ req.last_name }}</h3>
|
||||
<p class="request-email">{{ req.email }}</p>
|
||||
<p class="request-org">{{ (req.organisation)|spacify if req.organisation else '' }}{% if req.role %} - {{ (req.role)|spacify }}{% endif %}</p>
|
||||
<p class="request-org">{{ req.organisation if req.organisation else '' }}{% if req.role %} - {{ req.role }}{% endif %}</p>
|
||||
{% if req.introduction %}
|
||||
<p class="request-intro">"{{ req.introduction }}"</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
{% for conn in connections %}
|
||||
<div class="connection-card">
|
||||
<h4 style="margin-bottom: 12px;">{{ conn.first_name }} {{ conn.last_name }}</h4>
|
||||
<p class="connection-org" style="margin-bottom: 12px;">{{ (conn.organisation)|spacify if conn.organisation else '' }}</p>
|
||||
<p class="connection-role">{{ (conn.role)|spacify if conn.role else '' }}</p>
|
||||
<p class="connection-org" style="margin-bottom: 12px;">{{ conn.organisation if conn.organisation else '' }}</p>
|
||||
<p class="connection-role">{{ conn.role if conn.role else '' }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,14 @@
|
||||
<textarea id="introduction" name="introduction" rows="4">{{ attendee.introduction or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group switch-group">
|
||||
<label for="visible_to_others">{{ 'visible_to_attendees'|t }}</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="visible_to_others" name="visible_to_others" value="1" {% if attendee.visible_to_others %}checked{% endif %}>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'update_profile'|t }}</button>
|
||||
</div>
|
||||
@@ -110,6 +118,53 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.switch-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.switch-group label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 26px;
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
.current-photo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ 'forgot_password'|t }} - NetEvents{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-box">
|
||||
<h2>{{ 'forgot_password'|t }}</h2>
|
||||
<p class="auth-subtitle">{{ 'forgot_password_desc'|t }}</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('forgot_password') }}">
|
||||
<div class="form-group">
|
||||
<label for="email">{{ 'email'|t }}</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">{{ 'send_reset_link'|t }}</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-footer">
|
||||
{{ 'remember_password'|t }}
|
||||
<a href="{{ url_for('login') }}">{{ 'login'|t }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -28,6 +28,10 @@
|
||||
<a href="{{ url_for('register_organizer') }}">{{ 'register_as_organizer'|t }}</a>
|
||||
</p>
|
||||
|
||||
<p class="forgot-password-link">
|
||||
<a href="{{ url_for('forgot_password') }}">{{ 'forgot_password'|t }}</a>
|
||||
</p>
|
||||
|
||||
<div class="attendee-link-box">
|
||||
<p>{{ 'attendee_profile_link'|t }}</p>
|
||||
<p><a href="{{ url_for('request_attendee_link') }}"><strong>{{ 'click_here'|t }}</strong></a></p>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ 'reset_password'|t }} - NetEvents{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-box">
|
||||
<h2>{{ 'set_new_password'|t }}</h2>
|
||||
|
||||
<form method="POST" action="{{ url_for('reset_password', token=token) }}">
|
||||
<div class="form-group">
|
||||
<label for="password">{{ 'new_password'|t }}</label>
|
||||
<input type="password" id="password" name="password" required minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">{{ 'confirm_password'|t }}</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required minlength="6">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">{{ 'reset_password'|t }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -28,8 +28,8 @@
|
||||
<img src="{{ attendee.qr_code }}" alt="QR Code" width="100" height="100">
|
||||
</div>
|
||||
<div class="badge-body">
|
||||
<p class="badge-org">{{ (attendee.organisation)|spacify if attendee.organisation else '' }}</p>
|
||||
<p class="badge-role">{{ (attendee.role)|spacify if attendee.role else '' }}</p>
|
||||
<p class="badge-org">{{ attendee.organisation if attendee.organisation else '' }}</p>
|
||||
<p class="badge-role">{{ attendee.role if attendee.role else '' }}</p>
|
||||
{% if attendee.introduction %}
|
||||
<p class="badge-intro">{{ attendee.introduction[:80] }}{% if attendee.introduction|length > 80 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<tr>
|
||||
<td>{{ rsvp.first_name }} {{ rsvp.last_name }}</td>
|
||||
<td>{{ rsvp.email }}</td>
|
||||
<td>{{ (rsvp.organisation)|spacify if rsvp.organisation else '-' }}</td>
|
||||
<td>{{ (rsvp.role)|spacify if rsvp.role else '-' }}</td>
|
||||
<td>{{ rsvp.organisation if rsvp.organisation else '-' }}</td>
|
||||
<td>{{ rsvp.role if rsvp.role else '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}{{ 'create_breakout_session'|t }} - NetEvents{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container" style="padding: 20px;">
|
||||
<div class="form-container">
|
||||
<h1>{{ 'create_breakout_session'|t }}</h1>
|
||||
<p class="event-info">{{ 'event'|t }}: {{ event.name }}</p>
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="visible_to_others" name="visible_to_others" value="1" {% if attendee.visible_to_others %}checked{% endif %}>
|
||||
<label for="visible_to_others">{{ 'visible_to_attendees'|t }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'save_changes'|t }}</button>
|
||||
<a href="{{ url_for('organizer_dashboard') }}" class="btn btn-outline">{{ 'cancel'|t }}</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}{{ 'edit_breakout_session'|t }} - NetEvents{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container" style="padding: 20px;">
|
||||
<div class="form-container">
|
||||
<h1>{{ 'edit_breakout_session'|t }}</h1>
|
||||
<p class="event-info">{{ 'event'|t }}: {{ event.name }}</p>
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ th.sort-desc .sort-icon::before {
|
||||
|
||||
<section class="attendees-section">
|
||||
<div class="section-box">
|
||||
<h2>{{ 'registered_attendees'|t }}</h2>
|
||||
<h2>{{ 'registered_attendees'|t }} ({{ attendees|length }})</h2>
|
||||
<div class="section-actions">
|
||||
<a href="{{ url_for('register_attendee', code=event.code) }}" class="btn btn-primary">{{ 'add_attendee'|t }}</a>
|
||||
<a href="{{ url_for('event_badges', event_id=event.id) }}" class="btn btn-secondary">🖨️ {{ 'print_badges'|t }}</a>
|
||||
@@ -416,8 +416,8 @@ th.sort-desc .sort-icon::before {
|
||||
<input type="checkbox" name="attendee_ids" value="{{ attendee.id }}" class="attendee-checkbox" form="batch-assign-form">
|
||||
</td>
|
||||
<td>{{ attendee.first_name }} {{ attendee.last_name }}</td>
|
||||
<td>{{ (attendee.organisation)|spacify if attendee.organisation else '-' }}</td>
|
||||
<td>{{ (attendee.role)|spacify if attendee.role else '-' }}</td>
|
||||
<td>{{ attendee.organisation if attendee.organisation else '-' }}</td>
|
||||
<td>{{ attendee.role if attendee.role else '-' }}</td>
|
||||
<td>
|
||||
{% if attendee.attendee_type_name %}
|
||||
<span class="badge" style="background: #3498db; color: white;">{{ attendee.attendee_type_name }}</span>
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
<textarea id="introduction" name="introduction" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="visible_to_others" name="visible_to_others" value="1" checked>
|
||||
<label for="visible_to_others">{{ 'visible_to_attendees'|t }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{{ 'password'|t }}</label>
|
||||
<input type="password" id="password" name="password" required minlength="6">
|
||||
|
||||
Reference in New Issue
Block a user