From ef5ccc18f7e87209c8929b84f2b2a4d875424e13 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Sat, 27 Dec 2025 23:12:21 +0100 Subject: [PATCH] feat: add navigation bar with progress indicator and backward navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created _navigation.html component with 5-step progress indicator - Added German labels (E-Mail, Persönliche Daten, Motivation, Dokumente, Bestätigung) - Implemented can_access_page() validation in routes.py - Users can navigate backward to completed pages (data preserved) - Sequential forward progression enforced (cannot skip ahead) - Visual states: completed (green checkmark), current (blue), future (gray) - Added comprehensive CSS styling with responsive design - Updated base.html to include navigation component - Updated page templates to pass current_page context - Navigation appears on pages 2-5 only - All 102 tests passing with no regressions --- app/routes.py | 64 ++++++ prompts/completed/006-add-page-navigation.md | 208 +++++++++++++++++++ static/style.css | 146 +++++++++++++ templates/_navigation.html | 80 +++++++ templates/base.html | 3 + templates/page2_personal.html | 4 +- templates/page3_motivation.html | 1 + 7 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 prompts/completed/006-add-page-navigation.md create mode 100644 templates/_navigation.html diff --git a/app/routes.py b/app/routes.py index 3612ec2..c762b0b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -27,6 +27,25 @@ from app.utils import check_rate_limit def register_routes(app, mail): """Register all application routes""" + def can_access_page(session_id, requested_page): + """ + Determine if user can access the requested page. + + Rules: + - Can always access pages <= current_page (backward navigation) + - Cannot access pages > current_page (must progress sequentially) + - Page 1 is always accessible + """ + if requested_page == 1: + return True + + app_data = load_application_data(session_id) + if not app_data: + return False + + current_page = app_data.get('current_page', 1) + return requested_page <= current_page + @app.route('/') def index(): """Redirect to apply page""" @@ -91,6 +110,11 @@ def register_routes(app, mail): @app.route('/apply//personal') def page2_personal(session_id): """Page 2: Basic personal information""" + # Validate page access + if not can_access_page(session_id, 2): + flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') + return redirect(url_for('page1_email')) + app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') @@ -99,6 +123,7 @@ def register_routes(app, mail): return render_template('page2_personal.html', session_id=session_id, job_name=app_data['job_name'], + current_page=app_data.get('current_page', 2), data=app_data.get('personal_info', {})) @@ -180,6 +205,16 @@ def register_routes(app, mail): @app.route('/apply//motivation') def page3_motivation(session_id): """Page 3: Motivation and qualification questions""" + # Validate page access + if not can_access_page(session_id, 3): + app_data = load_application_data(session_id) + if app_data: + current_page = app_data.get('current_page', 2) + if current_page == 2: + return redirect(url_for('page2_personal', session_id=session_id)) + flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') + return redirect(url_for('page1_email')) + app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') @@ -188,6 +223,7 @@ def register_routes(app, mail): return render_template('page3_motivation.html', session_id=session_id, job_name=app_data['job_name'], + current_page=app_data.get('current_page', 3), data=app_data.get('motivation_answers', {})) @@ -249,6 +285,18 @@ def register_routes(app, mail): @app.route('/apply//upload') def page4_upload(session_id): """Page 4: Document upload""" + # Validate page access + if not can_access_page(session_id, 4): + app_data = load_application_data(session_id) + if app_data: + current_page = app_data.get('current_page', 2) + if current_page == 2: + return redirect(url_for('page2_personal', session_id=session_id)) + elif current_page == 3: + return redirect(url_for('page3_motivation', session_id=session_id)) + flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') + return redirect(url_for('page1_email')) + app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') @@ -257,6 +305,7 @@ def register_routes(app, mail): return render_template('page4_upload.html', session_id=session_id, job_name=app_data['job_name'], + current_page=app_data.get('current_page', 4), uploaded_files=app_data.get('uploaded_files', []), max_files=app.config['MAX_FILES']) @@ -395,6 +444,20 @@ def register_routes(app, mail): @app.route('/apply//confirmation') def page5_confirmation(session_id): """Page 5: Confirmation page""" + # Validate page access + if not can_access_page(session_id, 5): + app_data = load_application_data(session_id) + if app_data: + current_page = app_data.get('current_page', 2) + if current_page == 2: + return redirect(url_for('page2_personal', session_id=session_id)) + elif current_page == 3: + return redirect(url_for('page3_motivation', session_id=session_id)) + elif current_page == 4: + return redirect(url_for('page4_upload', session_id=session_id)) + flash('Sie müssen zuerst die vorherigen Schritte abschließen.', 'error') + return redirect(url_for('page1_email')) + app_data = load_application_data(session_id) if not app_data: flash('Bewerbung nicht gefunden.', 'error') @@ -403,6 +466,7 @@ def register_routes(app, mail): return render_template('page5_confirmation.html', session_id=session_id, job_name=app_data['job_name'], + current_page=app_data.get('current_page', 5), email=app_data['email']) diff --git a/prompts/completed/006-add-page-navigation.md b/prompts/completed/006-add-page-navigation.md new file mode 100644 index 0000000..6e1873d --- /dev/null +++ b/prompts/completed/006-add-page-navigation.md @@ -0,0 +1,208 @@ + +Add a navigation bar with progress indicator to the multi-page job application workflow, allowing users to navigate backward to previous pages to make changes while maintaining sequential forward progression. + +This will improve user experience by allowing applicants to review and edit their information without losing progress, reducing form abandonment and improving data quality. + + + +Current state: +- 5-page workflow: email capture → personal info → motivation → upload → confirmation +- Users can only move forward through pages +- No way to go back and edit previous pages after submission +- Pages are: page1_email.html, page2_personal.html, page3_motivation.html, page4_upload.html, page5_confirmation.html +- Uses session-based application tracking with session_id +- All data persisted to YAML files in app/models.py + +Target state: +- Progress indicator showing all 5 steps +- Users can click on completed steps (pages they've already visited) to go back +- Cannot skip ahead to pages they haven't reached yet +- Current page highlighted in progress bar +- Navigation preserves all previously entered data + +Review the current implementation: +@app/routes.py +@templates/page2_personal.html +@templates/page3_motivation.html +@templates/page4_upload.html + + + +1. **Create navigation component**: + - Add progress bar/breadcrumb navigation to base.html or as includable partial + - Show all 5 steps: Email → Personal → Motivation → Upload → Confirmation + - Visually distinguish between: completed (clickable), current (highlighted), future (disabled) + - Must be present on pages 2, 3, 4, 5 (not on page 1) + +2. **Navigation behavior**: + - **Backward navigation**: Users can click on any previously completed page to go back + - **Forward navigation**: Sequential only - users must click "Next" buttons, cannot skip ahead + - **Current page tracking**: Use `current_page` field in application data to determine which pages are accessible + - **Data preservation**: All previously entered data must be preserved when navigating + +3. **Visual design** (German labels): + - Step 1: "E-Mail" (page1_email.html) + - Step 2: "Daten" (page2_personal.html) + - Step 3: "Motivation" (page3_motivation.html) + - Step 4: "Dokumente" (page4_upload.html) + - Step 5: "Bestätigung" (page5_confirmation.html) + + - Completed steps: Green checkmark + clickable link + - Current step: Bold + highlighted background + - Future steps: Gray + no link + +4. **Implementation approach**: + - Add navigation partial to templates/ (e.g., `_navigation.html` or add to `base.html`) + - Pass `current_page` and `session_id` to all templates + - Add CSS styling for progress bar (can be inline in base.html or in style.css) + - Update route handlers to accept navigation from any completed page + - Ensure CSRF tokens work with navigation + +5. **Data handling**: + - When user navigates backward, load existing data from YAML + - Pre-populate forms with saved data + - When user submits, update data and allow forward navigation + - Don't lose uploaded files when navigating + +6. **Edge cases**: + - If user tries to access a page beyond current_page directly via URL, redirect to current page + - If session_id is invalid, redirect to page 1 + - On page 5 (confirmation), navigation can be read-only (all steps completed) + + + +**Suggested implementation strategy**: + +1. **Create navigation component**: + - Create `templates/_navigation.html` partial with Jinja2 template for progress bar + - OR add navigation directly to `base.html` as a conditional block + - Use Bootstrap-style progress steps or custom CSS + +2. **Update base.html**: + - Add navigation include or block after header + - Only show navigation if `session_id` exists and `current_page >= 2` + +3. **Update all page templates** (page2, page3, page4, page5): + - Include navigation component at top + - Pass `current_page`, `session_id`, `job_name` to navigation + +4. **Update route handlers** in `app/routes.py`: + - Add helper function to validate page access: `can_access_page(session_id, page_number)` + - Add navigation routes or update existing routes to handle backward navigation + - Ensure all routes check if user can access the requested page + +5. **CSS styling**: + - Add styles to `static/style.css` for progress bar + - Use semantic colors: green for completed, blue/highlight for current, gray for future + - Make clickable steps have hover effects + +**Example navigation structure** (German): +``` +[✓ E-Mail] → [✓ Daten] → [● Motivation] → [ Dokumente] → [ Bestätigung] + clickable clickable current disabled disabled +``` + +**WHY this approach**: +- Progress indicator provides clear visual feedback on completion status +- Backward navigation allows data correction without starting over +- Sequential forward validation ensures data quality and prevents skipping required fields +- Session-based tracking already in place, just need to leverage current_page +- Preserves all existing functionality while adding navigation + + + +Add helper function to check page access: + +```python +def can_access_page(session_id, requested_page): + """ + Determine if user can access the requested page. + + Rules: + - Can always access pages <= current_page (backward navigation) + - Cannot access pages > current_page (must progress sequentially) + - Page 1 is always accessible + """ + if requested_page == 1: + return True + + app_data = load_application_data(session_id) + if not app_data: + return False + + current_page = app_data.get('current_page', 1) + return requested_page <= current_page +``` + +Use in route handlers: +```python +@app.route('/apply//personal') +def page2_personal(session_id): + if not can_access_page(session_id, 2): + return redirect(url_for('page1_email')) + # ... rest of handler +``` + + + +Modify the following files: + +- `./templates/base.html` - Add navigation component or include statement +- `./templates/_navigation.html` (NEW) - Progress bar component (if using partial approach) +- `./templates/page2_personal.html` - Ensure navigation is displayed +- `./templates/page3_motivation.html` - Ensure navigation is displayed +- `./templates/page4_upload.html` - Ensure navigation is displayed +- `./templates/page5_confirmation.html` - Ensure navigation is displayed (read-only mode) +- `./app/routes.py` - Add `can_access_page()` helper and update route guards +- `./static/style.css` - Add navigation styling + +Do NOT modify: +- Data models or storage logic +- Form validation logic +- Email functionality +- File upload logic + + + +Before declaring complete, verify: + +1. **Visual verification**: + - Progress bar appears on pages 2, 3, 4, 5 + - Current step is highlighted + - Completed steps show checkmarks and are clickable + - Future steps are grayed out and not clickable + +2. **Navigation testing**: + - From page 3, click on "Daten" → should go to page 2 with data pre-populated + - From page 4, click on "E-Mail" → should go to page 1 with email shown + - Try to access page 4 by typing URL when on page 2 → should redirect to page 2 + - Complete workflow and verify navigation works at each step + +3. **Data preservation**: + - Fill out page 2, go to page 3, go back to page 2 → all fields still filled + - Upload files on page 4, go back to page 3, return to page 4 → files still there + - Make changes on a previous page and save → changes persisted + +4. **Edge cases**: + - Invalid session_id → redirect to page 1 + - Accessing page directly via URL beyond current_page → redirect to current page + - Page 5 navigation is read-only (all steps completed) + +5. **Tests still pass**: + - Run `pytest tests/ -v` to ensure no regressions + - Existing tests should pass (they test form submission, not navigation) + - Consider adding new tests for navigation behavior (optional) + + + +- Progress bar/breadcrumb navigation visible on pages 2-5 +- Users can navigate backward to any completed page +- Users cannot skip ahead to uncompleted pages +- All previously entered data is preserved when navigating +- Current page is visually distinct in navigation +- Navigation uses German labels matching the application +- All existing functionality still works +- No test regressions +- Clean, professional styling that matches existing design + +Completed: Sa 27 Dez 2025 23:11:04 CET diff --git a/static/style.css b/static/style.css index a531669..7d7b166 100644 --- a/static/style.css +++ b/static/style.css @@ -366,6 +366,123 @@ header h1 { margin: 20px 0; } +/* Progress Navigation */ +.progress-navigation { + margin-bottom: 30px; + background-color: #f8f9fa; + padding: 20px; + border-radius: 8px; + border: 1px solid #ddd; +} + +.progress-steps { + list-style: none; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + counter-reset: step; +} + +.progress-step { + flex: 1; + text-align: center; + position: relative; +} + +.progress-step:not(:last-child)::after { + content: ''; + position: absolute; + top: 20px; + left: 50%; + width: 100%; + height: 2px; + background-color: #ddd; + z-index: 0; +} + +.progress-step.completed:not(:last-child)::after { + background-color: #28a745; +} + +.progress-link { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-decoration: none; + color: #666; + position: relative; + z-index: 1; +} + +.progress-step.completed .progress-link { + color: #28a745; + cursor: pointer; +} + +.progress-step.active .progress-link { + color: #007bff; + font-weight: bold; +} + +.progress-step.disabled .progress-link { + color: #999; + cursor: not-allowed; +} + +.progress-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.1em; + background-color: white; + border: 2px solid #ddd; + transition: all 0.2s; +} + +.progress-step.completed .progress-icon { + background-color: #28a745; + border-color: #28a745; + color: white; +} + +.progress-step.active .progress-icon { + background-color: #007bff; + border-color: #007bff; + color: white; +} + +.progress-step.disabled .progress-icon { + background-color: #f8f9fa; + border-color: #ddd; + color: #999; +} + +.progress-step.completed a.progress-link:hover .progress-icon { + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3); +} + +.progress-step.completed a.progress-link:hover { + color: #1e7e34; +} + +.progress-label { + font-size: 0.85em; + display: block; + max-width: 100px; + word-wrap: break-word; +} + +.progress-step.active .progress-label { + font-weight: bold; +} + /* Footer */ footer { margin-top: 40px; @@ -411,4 +528,33 @@ footer { .page-intro h2 { font-size: 1.2em; } + + /* Progress Navigation Mobile */ + .progress-navigation { + padding: 15px 10px; + } + + .progress-steps { + flex-wrap: wrap; + gap: 15px; + } + + .progress-step { + flex: 0 0 calc(50% - 8px); + } + + .progress-step:not(:last-child)::after { + display: none; + } + + .progress-icon { + width: 35px; + height: 35px; + font-size: 1em; + } + + .progress-label { + font-size: 0.75em; + max-width: 80px; + } } diff --git a/templates/_navigation.html b/templates/_navigation.html new file mode 100644 index 0000000..d109a79 --- /dev/null +++ b/templates/_navigation.html @@ -0,0 +1,80 @@ +{% if session_id and current_page and current_page >= 2 %} + +{% endif %} diff --git a/templates/base.html b/templates/base.html index 711e09d..f69ebff 100644 --- a/templates/base.html +++ b/templates/base.html @@ -12,6 +12,9 @@

{% block header %}Bewerbungsformular{% endblock %}

+ + {% include '_navigation.html' %} +
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} diff --git a/templates/page2_personal.html b/templates/page2_personal.html index b283a77..4691d41 100644 --- a/templates/page2_personal.html +++ b/templates/page2_personal.html @@ -1,12 +1,12 @@ {% extends "base.html" %} -{% block title %}Bewerbung - Persönliche Daten{% endblock %} +{% block title %}Bewerbung - Daten{% endblock %} {% block header %}Bewerbung: {{ job_name }}{% endblock %} {% block content %}
-

Schritt 1 von 3: Persönliche Daten

+

Schritt 1 von 3: Daten

Bitte geben Sie Ihre persönlichen Informationen ein. Alle Felder mit * sind Pflichtfelder.

diff --git a/templates/page3_motivation.html b/templates/page3_motivation.html index 4758653..5800e53 100644 --- a/templates/page3_motivation.html +++ b/templates/page3_motivation.html @@ -8,6 +8,7 @@

Schritt 2 von 3: Motivation und Qualifikationen

Bitte beantworten Sie die folgenden Fragen. Alle Angaben sind optional, helfen uns aber, Sie besser kennenzulernen.

+

Falls sie ein Motivationsschreiben einreichen wollen haben Sie im nächsten Schritt die Möglichkeit dazu.

Maximal ca. 3000 Zeichen pro Antwort (etwa eine A4-Seite)