<div {{ stimulus_controller('@pwa/contact-picker') }}>
<div class="contact-manager">
<header class="app-header">
<h1>Contact Manager</h1>
<button {{ stimulus_action('@pwa/contact-picker', 'select', 'click', {
multiple: true
}) }}>
📱 Import from Device
</button>
</header>
<div class="search-bar">
<input type="text" id="search" placeholder="Search contacts...">
</div>
<div class="contacts-grid" id="contacts-grid">
<p class="empty-state">No contacts yet. Import from your device to get started.</p>
</div>
<div id="contact-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="closeModal()">×</span>
<h2 id="modal-title">Contact Details</h2>
<div id="modal-body"></div>
</div>
</div>
</div>
</div>
<script>
let contacts = JSON.parse(localStorage.getItem('contacts') || '[]');
document.addEventListener('pwa--contact-picker:selection', (event) => {
const { contacts: newContacts } = event.detail;
newContacts.forEach(contact => {
const formattedContact = {
id: Date.now() + Math.random(),
name: contact.name ? contact.name[0] : 'Unknown',
emails: contact.email || [],
phones: contact.tel || [],
addresses: contact.address || [],
importedAt: new Date().toISOString()
};
// Check for duplicates
const isDuplicate = contacts.some(c =>
(formattedContact.emails.length > 0 && c.emails.includes(formattedContact.emails[0])) ||
(formattedContact.phones.length > 0 && c.phones.includes(formattedContact.phones[0]))
);
if (!isDuplicate) {
contacts.push(formattedContact);
}
});
saveContacts();
renderContacts();
});
document.addEventListener('pwa--contact-picker:unavailable', () => {
alert('Contact Picker is not available on this device. You can still add contacts manually.');
});
document.getElementById('search').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
renderContacts(query);
});
function renderContacts(searchQuery = '') {
const grid = document.getElementById('contacts-grid');
let filtered = contacts;
if (searchQuery) {
filtered = contacts.filter(contact =>
contact.name.toLowerCase().includes(searchQuery) ||
contact.emails.some(email => email.toLowerCase().includes(searchQuery)) ||
contact.phones.some(phone => phone.includes(searchQuery))
);
}
if (filtered.length === 0) {
grid.innerHTML = '<p class="empty-state">No contacts found.</p>';
return;
}
const cardsHtml = filtered.map(contact => `
<div class="contact-card" onclick="showContactDetails('${contact.id}')">
<div class="contact-avatar">${contact.name.charAt(0).toUpperCase()}</div>
<div class="contact-info">
<h3>${contact.name}</h3>
${contact.emails.length > 0 ? `<p>📧 ${contact.emails[0]}</p>` : ''}
${contact.phones.length > 0 ? `<p>📱 ${contact.phones[0]}</p>` : ''}
</div>
<button class="delete-btn" onclick="event.stopPropagation(); deleteContact('${contact.id}')">
🗑️
</button>
</div>
`).join('');
grid.innerHTML = cardsHtml;
}
function showContactDetails(contactId) {
const contact = contacts.find(c => c.id == contactId);
if (!contact) return;
const modal = document.getElementById('contact-modal');
const modalTitle = document.getElementById('modal-title');
const modalBody = document.getElementById('modal-body');
modalTitle.textContent = contact.name;
modalBody.innerHTML = `
<div class="detail-section">
<h3>Emails</h3>
${contact.emails.length > 0 ?
contact.emails.map(email => `<p><a href="mailto:${email}">${email}</a></p>`).join('') :
'<p class="no-data">No emails</p>'
}
</div>
<div class="detail-section">
<h3>Phone Numbers</h3>
${contact.phones.length > 0 ?
contact.phones.map(phone => `<p><a href="tel:${phone}">${phone}</a></p>`).join('') :
'<p class="no-data">No phone numbers</p>'
}
</div>
<div class="detail-section">
<h3>Addresses</h3>
${contact.addresses.length > 0 ?
contact.addresses.map(addr => `<p>${addr}</p>`).join('') :
'<p class="no-data">No addresses</p>'
}
</div>
<div class="detail-section">
<h3>Import Info</h3>
<p>Imported: ${new Date(contact.importedAt).toLocaleString()}</p>
</div>
`;
modal.style.display = 'block';
}
function closeModal() {
document.getElementById('contact-modal').style.display = 'none';
}
function deleteContact(contactId) {
if (!confirm('Are you sure you want to delete this contact?')) {
return;
}
contacts = contacts.filter(c => c.id != contactId);
saveContacts();
renderContacts();
}
function saveContacts() {
localStorage.setItem('contacts', JSON.stringify(contacts));
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('contact-modal');
if (event.target == modal) {
closeModal();
}
}
// Initial render
renderContacts();
</script>
<style>
.contact-manager {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.app-header button {
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
.search-bar {
margin-bottom: 30px;
}
.search-bar input {
width: 100%;
padding: 12px 20px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
}
.contacts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
color: #9ca3af;
font-size: 18px;
}
.contact-card {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.contact-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.contact-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
}
.contact-info {
flex: 1;
}
.contact-info h3 {
margin: 0 0 5px 0;
font-size: 18px;
}
.contact-info p {
margin: 3px 0;
font-size: 14px;
color: #6b7280;
}
.delete-btn {
padding: 8px;
background: #fee2e2;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
transition: background 0.2s;
}
.delete-btn:hover {
background: #ef4444;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background-color: white;
margin: 5% auto;
padding: 30px;
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.close {
position: absolute;
right: 20px;
top: 20px;
font-size: 28px;
font-weight: bold;
color: #9ca3af;
cursor: pointer;
}
.close:hover {
color: #1f2937;
}
.detail-section {
margin: 25px 0;
padding-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.detail-section:last-child {
border-bottom: none;
}
.detail-section h3 {
margin: 0 0 15px 0;
color: #6b7280;
font-size: 14px;
text-transform: uppercase;
}
.detail-section p {
margin: 8px 0;
font-size: 16px;
}
.detail-section a {
color: #3b82f6;
text-decoration: none;
}
.detail-section a:hover {
text-decoration: underline;
}
.no-data {
color: #9ca3af;
font-style: italic;
}
</style>