Christian Kniep
new webapp
1fff71f
{% 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 %}