Spaces:
Sleeping
Sleeping
| {% extends "base.html" %} | |
| {% block title %}My Contacts - PrepMate{% endblock %} | |
| {% block content %} | |
| <div class="container mt-4"> | |
| <!-- Header with Actions --> | |
| <div class="row mb-4"> | |
| <div class="col-md-6"> | |
| <h1 class="h3"> | |
| <i class="bi bi-people-fill"></i> My Contacts | |
| </h1> | |
| <p class="text-muted">Manage your contact sessions ({{ contact_count }}/500)</p> | |
| </div> | |
| <div class="col-md-6 text-end"> | |
| <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newContactModal"> | |
| <i class="bi bi-plus-circle"></i> New Contact | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Search and Sort Controls --> | |
| <div class="row mb-3"> | |
| <div class="col-md-8"> | |
| <div class="input-group"> | |
| <span class="input-group-text"> | |
| <i class="bi bi-search"></i> | |
| </span> | |
| <input type="text" | |
| class="form-control" | |
| id="searchInput" | |
| placeholder="Search contacts by name or description..." | |
| value="{{ search_query }}" | |
| autocomplete="off"> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <select class="form-select" id="sortSelect"> | |
| <option value="recent" {% if sort_order == 'recent' %}selected{% endif %}>Most Recent</option> | |
| <option value="alphabetical" {% if sort_order == 'alphabetical' %}selected{% endif %}>Alphabetical</option> | |
| <option value="oldest" {% if sort_order == 'oldest' %}selected{% endif %}>Oldest First</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Contacts List --> | |
| <div class="row"> | |
| <div class="col-12"> | |
| {% if contacts %} | |
| <div class="list-group" id="contactsList"> | |
| {% for contact in contacts %} | |
| <a href="{{ url_for('contacts.view_contact', session_id=contact.session_id) }}" | |
| class="list-group-item list-group-item-action contact-card"> | |
| <div class="d-flex w-100 justify-content-between align-items-start"> | |
| <div class="flex-grow-1"> | |
| <h5 class="mb-1"> | |
| <i class="bi bi-person-circle text-primary me-2"></i> | |
| {{ contact.contact_name }} | |
| </h5> | |
| {% if contact.contact_description %} | |
| <p class="mb-1 text-muted small">{{ contact.contact_description }}</p> | |
| {% endif %} | |
| <small class="text-muted"> | |
| <i class="bi bi-clock"></i> | |
| Last interaction: <span class="relative-time" data-timestamp="{{ contact.last_interaction.isoformat() }}"> | |
| {{ contact.last_interaction.strftime('%Y-%m-%d %H:%M') }} | |
| </span> | |
| </small> | |
| </div> | |
| <div class="ms-3"> | |
| <i class="bi bi-chevron-right"></i> | |
| </div> | |
| </div> | |
| </a> | |
| {% endfor %} | |
| </div> | |
| {% else %} | |
| <!-- Empty State --> | |
| <div class="empty-state text-center py-5"> | |
| <i class="bi bi-inbox" style="font-size: 4rem; color: #6c757d;"></i> | |
| <h3 class="mt-3">No contacts yet</h3> | |
| {% if search_query %} | |
| <p class="text-muted"> | |
| No contacts match your search for "{{ search_query }}". | |
| <a href="{{ url_for('contacts.list_contacts') }}">Clear search</a> | |
| </p> | |
| {% else %} | |
| <p class="text-muted"> | |
| Create your first contact to start building your relationship context. | |
| </p> | |
| <button type="button" class="btn btn-primary btn-lg mt-3" data-bs-toggle="modal" data-bs-target="#newContactModal"> | |
| <i class="bi bi-plus-circle"></i> Create First Contact | |
| </button> | |
| {% endif %} | |
| </div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- New Contact Modal --> | |
| <div class="modal fade" id="newContactModal" tabindex="-1" aria-labelledby="newContactModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content"> | |
| <form method="POST" action="{{ url_for('contacts.create_contact') }}" id="newContactForm"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title" id="newContactModalLabel"> | |
| <i class="bi bi-person-plus"></i> New Contact | |
| </h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="mb-3"> | |
| <label for="contactName" class="form-label"> | |
| Contact Name <span class="text-danger">*</span> | |
| </label> | |
| <input type="text" | |
| class="form-control" | |
| id="contactName" | |
| name="contact_name" | |
| maxlength="255" | |
| required | |
| placeholder="e.g., Jane Doe" | |
| autocomplete="off"> | |
| <div class="form-text"> | |
| <span id="nameCounter">0 / 255</span> characters | |
| </div> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="contactDescription" class="form-label"> | |
| Description (Optional) | |
| </label> | |
| <textarea class="form-control" | |
| id="contactDescription" | |
| name="contact_description" | |
| rows="3" | |
| maxlength="500" | |
| placeholder="e.g., College roommate, works at Tech Corp"></textarea> | |
| <div class="form-text"> | |
| <span id="descCounter">0 / 500</span> characters | |
| </div> | |
| </div> | |
| <div class="alert alert-info" role="alert"> | |
| <i class="bi bi-info-circle"></i> | |
| This will create a new session to track your interactions and facts about this contact. | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
| <button type="submit" class="btn btn-primary" id="createContactBtn"> | |
| <i class="bi bi-check-circle"></i> Create Contact | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Character counters | |
| const contactName = document.getElementById('contactName'); | |
| const contactDescription = document.getElementById('contactDescription'); | |
| const nameCounter = document.getElementById('nameCounter'); | |
| const descCounter = document.getElementById('descCounter'); | |
| if (contactName) { | |
| contactName.addEventListener('input', function() { | |
| nameCounter.textContent = this.value.length + ' / 255'; | |
| if (this.value.length > 240) { | |
| nameCounter.classList.add('text-warning'); | |
| } else { | |
| nameCounter.classList.remove('text-warning'); | |
| } | |
| }); | |
| } | |
| if (contactDescription) { | |
| contactDescription.addEventListener('input', function() { | |
| descCounter.textContent = this.value.length + ' / 500'; | |
| if (this.value.length > 450) { | |
| descCounter.classList.add('text-warning'); | |
| } else { | |
| descCounter.classList.remove('text-warning'); | |
| } | |
| }); | |
| } | |
| // Search functionality with debounce | |
| let searchTimeout; | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) { | |
| searchInput.addEventListener('input', function() { | |
| clearTimeout(searchTimeout); | |
| searchTimeout = setTimeout(() => { | |
| const searchValue = this.value.trim(); | |
| const currentUrl = new URL(window.location); | |
| if (searchValue) { | |
| currentUrl.searchParams.set('search', searchValue); | |
| } else { | |
| currentUrl.searchParams.delete('search'); | |
| } | |
| window.location.href = currentUrl.toString(); | |
| }, 300); | |
| }); | |
| } | |
| // Sort functionality | |
| const sortSelect = document.getElementById('sortSelect'); | |
| if (sortSelect) { | |
| sortSelect.addEventListener('change', function() { | |
| const currentUrl = new URL(window.location); | |
| currentUrl.searchParams.set('sort', this.value); | |
| window.location.href = currentUrl.toString(); | |
| }); | |
| } | |
| // Relative time display | |
| function updateRelativeTimes() { | |
| const timeElements = document.querySelectorAll('.relative-time'); | |
| timeElements.forEach(element => { | |
| const timestamp = element.dataset.timestamp; | |
| if (!timestamp) return; | |
| const date = new Date(timestamp); | |
| const now = new Date(); | |
| const diffMs = now - date; | |
| const diffMins = Math.floor(diffMs / 60000); | |
| const diffHours = Math.floor(diffMs / 3600000); | |
| const diffDays = Math.floor(diffMs / 86400000); | |
| let relativeTime; | |
| if (diffMins < 1) { | |
| relativeTime = 'just now'; | |
| } else if (diffMins < 60) { | |
| relativeTime = diffMins + (diffMins === 1 ? ' minute ago' : ' minutes ago'); | |
| } else if (diffHours < 24) { | |
| relativeTime = diffHours + (diffHours === 1 ? ' hour ago' : ' hours ago'); | |
| } else if (diffDays < 7) { | |
| relativeTime = diffDays + (diffDays === 1 ? ' day ago' : ' days ago'); | |
| } else { | |
| relativeTime = date.toLocaleDateString(); | |
| } | |
| element.textContent = relativeTime; | |
| }); | |
| } | |
| // Update relative times on page load and every minute | |
| updateRelativeTimes(); | |
| setInterval(updateRelativeTimes, 60000); | |
| // Form validation | |
| const newContactForm = document.getElementById('newContactForm'); | |
| if (newContactForm) { | |
| newContactForm.addEventListener('submit', function(e) { | |
| const name = contactName.value.trim(); | |
| if (!name) { | |
| e.preventDefault(); | |
| alert('Contact name is required.'); | |
| contactName.focus(); | |
| return false; | |
| } | |
| if (name.length > 255) { | |
| e.preventDefault(); | |
| alert('Contact name cannot exceed 255 characters.'); | |
| contactName.focus(); | |
| return false; | |
| } | |
| const desc = contactDescription.value.trim(); | |
| if (desc.length > 500) { | |
| e.preventDefault(); | |
| alert('Description cannot exceed 500 characters.'); | |
| contactDescription.focus(); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| } | |
| </script> | |
| {% endblock %} | |