Size
37.2 KB
Version
1.1.1
Created
Feb 3, 2026
Updated
14 days ago
1// ==UserScript==
2// @name Instagram CRM Manager
3// @description A new extension
4// @version 1.1.1
5// @match https://*.instagram.com/*
6// @icon https://static.cdninstagram.com/rsrc.php/y4/r/QaBlI0OZiks.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // Utility function to debounce
12 function debounce(func, wait) {
13 let timeout;
14 return function executedFunction(...args) {
15 const later = () => {
16 clearTimeout(timeout);
17 func(...args);
18 };
19 clearTimeout(timeout);
20 timeout = setTimeout(later, wait);
21 };
22 }
23
24 // CRM Data Manager
25 class CRMDataManager {
26 constructor() {
27 this.contacts = [];
28 this.initialized = false;
29 }
30
31 async init() {
32 if (this.initialized) return;
33 const data = await GM.getValue('instagram_crm_contacts', '[]');
34 this.contacts = JSON.parse(data);
35 this.initialized = true;
36 console.log('CRM Data Manager initialized with', this.contacts.length, 'contacts');
37 }
38
39 async saveContacts() {
40 await GM.setValue('instagram_crm_contacts', JSON.stringify(this.contacts));
41 console.log('Contacts saved:', this.contacts.length);
42 }
43
44 async addContact(contactData) {
45 const existingIndex = this.contacts.findIndex(c => c.username === contactData.username);
46
47 if (existingIndex >= 0) {
48 // Update existing contact
49 this.contacts[existingIndex] = {
50 ...this.contacts[existingIndex],
51 ...contactData,
52 lastUpdated: new Date().toISOString()
53 };
54 console.log('Updated existing contact:', contactData.username);
55 } else {
56 // Add new contact
57 const newContact = {
58 id: Date.now().toString(),
59 ...contactData,
60 notes: contactData.notes || '',
61 tags: contactData.tags || [],
62 dateAdded: new Date().toISOString(),
63 lastUpdated: new Date().toISOString(),
64 interactions: contactData.interactions || [],
65 lastContactDate: contactData.lastContactDate || null,
66 reminderInterval: contactData.reminderInterval || 3
67 };
68 this.contacts.unshift(newContact);
69 console.log('Added new contact:', contactData.username);
70 }
71
72 await this.saveContacts();
73 return true;
74 }
75
76 async updateContact(id, updates) {
77 const index = this.contacts.findIndex(c => c.id === id);
78 if (index >= 0) {
79 this.contacts[index] = {
80 ...this.contacts[index],
81 ...updates,
82 lastUpdated: new Date().toISOString()
83 };
84 await this.saveContacts();
85 return true;
86 }
87 return false;
88 }
89
90 async deleteContact(id) {
91 const index = this.contacts.findIndex(c => c.id === id);
92 if (index >= 0) {
93 this.contacts.splice(index, 1);
94 await this.saveContacts();
95 return true;
96 }
97 return false;
98 }
99
100 getContacts() {
101 return this.contacts;
102 }
103
104 searchContacts(query) {
105 const lowerQuery = query.toLowerCase();
106 return this.contacts.filter(contact =>
107 contact.username.toLowerCase().includes(lowerQuery) ||
108 contact.fullName?.toLowerCase().includes(lowerQuery) ||
109 contact.notes?.toLowerCase().includes(lowerQuery) ||
110 contact.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
111 );
112 }
113
114 getOverdueContacts() {
115 const now = new Date();
116 return this.contacts.filter(contact => {
117 if (!contact.lastContactDate) return false;
118 const lastContact = new Date(contact.lastContactDate);
119 const daysSinceContact = Math.floor((now - lastContact) / (1000 * 60 * 60 * 24));
120 const reminderInterval = contact.reminderInterval || 3;
121 return daysSinceContact >= reminderInterval;
122 });
123 }
124
125 async markAsContacted(id) {
126 const index = this.contacts.findIndex(c => c.id === id);
127 if (index >= 0) {
128 this.contacts[index].lastContactDate = new Date().toISOString();
129 this.contacts[index].lastUpdated = new Date().toISOString();
130 await this.saveContacts();
131 return true;
132 }
133 return false;
134 }
135 }
136
137 // CRM UI Manager
138 class CRMUIManager {
139 constructor(dataManager) {
140 this.dataManager = dataManager;
141 this.panel = null;
142 this.isOpen = false;
143 this.currentView = 'list'; // 'list' or 'detail'
144 this.selectedContact = null;
145 }
146
147 createPanel() {
148 if (this.panel) return;
149
150 const panel = document.createElement('div');
151 panel.id = 'instagram-crm-panel';
152 panel.style.cssText = `
153 position: fixed;
154 top: 0;
155 right: -400px;
156 width: 400px;
157 height: 100vh;
158 background: #ffffff;
159 box-shadow: -2px 0 10px rgba(0,0,0,0.2);
160 z-index: 999999;
161 transition: right 0.3s ease;
162 display: flex;
163 flex-direction: column;
164 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
165 `;
166
167 panel.innerHTML = `
168 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; display: flex; justify-content: space-between; align-items: center;">
169 <h2 style="margin: 0; font-size: 20px; font-weight: 600;">Instagram CRM</h2>
170 <button id="crm-close-btn" style="background: rgba(255,255,255,0.2); border: none; color: white; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center;">×</button>
171 </div>
172
173 <div style="padding: 15px; border-bottom: 1px solid #dbdbdb;">
174 <input type="text" id="crm-search" placeholder="Search contacts..." style="width: 100%; padding: 10px; border: 1px solid #dbdbdb; border-radius: 8px; font-size: 14px; box-sizing: border-box;">
175 </div>
176
177 <div id="crm-content" style="flex: 1; overflow-y: auto; padding: 15px;">
178 <!-- Content will be dynamically loaded here -->
179 </div>
180
181 <div style="padding: 15px; border-top: 1px solid #dbdbdb; background: #fafafa;">
182 <div style="font-size: 12px; color: #8e8e8e; text-align: center;">
183 <span id="crm-contact-count">0</span> contacts saved
184 </div>
185 </div>
186 `;
187
188 document.body.appendChild(panel);
189 this.panel = panel;
190
191 // Event listeners
192 panel.querySelector('#crm-close-btn').addEventListener('click', () => this.togglePanel());
193 panel.querySelector('#crm-search').addEventListener('input', debounce((e) => {
194 this.handleSearch(e.target.value);
195 }, 300));
196
197 console.log('CRM Panel created');
198 }
199
200 createToggleButton() {
201 const button = document.createElement('button');
202 button.id = 'crm-toggle-btn';
203 button.innerHTML = '👥';
204 button.title = 'Open CRM Manager';
205 button.style.cssText = `
206 position: fixed;
207 bottom: 30px;
208 right: 30px;
209 width: 60px;
210 height: 60px;
211 border-radius: 50%;
212 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
213 color: white;
214 border: none;
215 font-size: 28px;
216 cursor: pointer;
217 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
218 z-index: 999998;
219 transition: transform 0.2s ease;
220 display: flex;
221 align-items: center;
222 justify-content: center;
223 position: relative;
224 `;
225
226 button.addEventListener('mouseenter', () => {
227 button.style.transform = 'scale(1.1)';
228 });
229
230 button.addEventListener('mouseleave', () => {
231 button.style.transform = 'scale(1)';
232 });
233
234 button.addEventListener('click', () => this.togglePanel());
235
236 document.body.appendChild(button);
237 this.updateBadge();
238 console.log('CRM Toggle button created');
239 }
240
241 togglePanel() {
242 this.isOpen = !this.isOpen;
243 if (this.isOpen) {
244 this.panel.style.right = '0';
245 this.renderContactList();
246 } else {
247 this.panel.style.right = '-400px';
248 }
249 }
250
251 updateBadge() {
252 const overdueCount = this.dataManager.getOverdueContacts().length;
253 const button = document.getElementById('crm-toggle-btn');
254 if (!button) return;
255
256 let badge = document.getElementById('crm-badge');
257 if (overdueCount > 0) {
258 if (!badge) {
259 badge = document.createElement('div');
260 badge.id = 'crm-badge';
261 badge.style.cssText = `
262 position: absolute;
263 top: -5px;
264 right: -5px;
265 background: #ed4956;
266 color: white;
267 border-radius: 50%;
268 width: 24px;
269 height: 24px;
270 display: flex;
271 align-items: center;
272 justify-content: center;
273 font-size: 12px;
274 font-weight: bold;
275 border: 2px solid white;
276 `;
277 button.appendChild(badge);
278 }
279 badge.textContent = overdueCount > 99 ? '99+' : overdueCount;
280 } else if (badge) {
281 badge.remove();
282 }
283 }
284
285 renderContactList() {
286 const content = this.panel.querySelector('#crm-content');
287 const contacts = this.dataManager.getContacts();
288
289 this.panel.querySelector('#crm-contact-count').textContent = contacts.length;
290
291 if (contacts.length === 0) {
292 content.innerHTML = `
293 <div style="text-align: center; padding: 40px 20px; color: #8e8e8e;">
294 <div style="font-size: 48px; margin-bottom: 15px;">📋</div>
295 <p style="margin: 0; font-size: 16px; font-weight: 500;">No contacts yet</p>
296 <p style="margin: 10px 0 0 0; font-size: 14px;">Visit Instagram profiles to add contacts</p>
297 </div>
298 `;
299 return;
300 }
301
302 const overdueContacts = this.dataManager.getOverdueContacts();
303 const overdueIds = new Set(overdueContacts.map(c => c.id));
304
305 let html = '<div style="display: flex; flex-direction: column; gap: 10px;">';
306
307 contacts.forEach(contact => {
308 const initials = contact.fullName ? contact.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) : contact.username.slice(0, 2).toUpperCase();
309 const avatarColor = this.getColorFromString(contact.username);
310 const isOverdue = overdueIds.has(contact.id);
311
312 html += `
313 <div class="crm-contact-card" data-id="${contact.id}" style="background: white; border: 1px solid ${isOverdue ? '#ed4956' : '#dbdbdb'}; border-radius: 12px; padding: 15px; cursor: pointer; transition: all 0.2s ease; ${isOverdue ? 'box-shadow: 0 0 0 2px rgba(237, 73, 86, 0.1);' : ''}">
314 <div style="display: flex; align-items: center; gap: 12px;">
315 <div style="width: 50px; height: 50px; border-radius: 50%; background: ${avatarColor}; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 16px; flex-shrink: 0; position: relative;">
316 ${contact.profilePic ? `<img src="${contact.profilePic}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">` : initials}
317 ${isOverdue ? '<div style="position: absolute; top: -3px; right: -3px; width: 16px; height: 16px; background: #ed4956; border: 2px solid white; border-radius: 50%;"></div>' : ''}
318 </div>
319 <div style="flex: 1; min-width: 0;">
320 <div style="font-weight: 600; font-size: 14px; color: #262626; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
321 ${contact.fullName || contact.username}
322 ${isOverdue ? '<span style="color: #ed4956; margin-left: 5px;">⏰</span>' : ''}
323 </div>
324 <div style="font-size: 13px; color: #8e8e8e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
325 @${contact.username}
326 </div>
327 ${contact.tags && contact.tags.length > 0 ? `
328 <div style="margin-top: 5px; display: flex; gap: 5px; flex-wrap: wrap;">
329 ${contact.tags.slice(0, 2).map(tag => `<span style="background: #e0e7ff; color: #4c51bf; padding: 2px 8px; border-radius: 12px; font-size: 11px;">${tag}</span>`).join('')}
330 </div>
331 ` : ''}
332 </div>
333 </div>
334 </div>
335 `;
336 });
337
338 html += '</div>';
339 content.innerHTML = html;
340
341 // Add click listeners to contact cards
342 content.querySelectorAll('.crm-contact-card').forEach(card => {
343 card.addEventListener('mouseenter', () => {
344 card.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
345 card.style.transform = 'translateY(-2px)';
346 });
347 card.addEventListener('mouseleave', () => {
348 const contactId = card.getAttribute('data-id');
349 const isOverdue = overdueIds.has(contactId);
350 card.style.boxShadow = isOverdue ? '0 0 0 2px rgba(237, 73, 86, 0.1)' : 'none';
351 card.style.transform = 'translateY(0)';
352 });
353 card.addEventListener('click', () => {
354 const contactId = card.getAttribute('data-id');
355 this.showContactDetail(contactId);
356 });
357 });
358 }
359
360 showContactDetail(contactId) {
361 const contact = this.dataManager.getContacts().find(c => c.id === contactId);
362 if (!contact) return;
363
364 this.selectedContact = contact;
365 const content = this.panel.querySelector('#crm-content');
366
367 const initials = contact.fullName ? contact.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) : contact.username.slice(0, 2).toUpperCase();
368 const avatarColor = this.getColorFromString(contact.username);
369
370 const lastContactDate = contact.lastContactDate ? new Date(contact.lastContactDate) : null;
371 const daysSinceContact = lastContactDate ? Math.floor((new Date() - lastContactDate) / (1000 * 60 * 60 * 24)) : null;
372 const reminderInterval = contact.reminderInterval || 3;
373 const isOverdue = daysSinceContact !== null && daysSinceContact >= reminderInterval;
374
375 content.innerHTML = `
376 <div>
377 <button id="crm-back-btn" style="background: none; border: none; color: #667eea; cursor: pointer; font-size: 14px; padding: 0 0 15px 0; display: flex; align-items: center; gap: 5px;">
378 ← Back to list
379 </button>
380
381 <div style="text-align: center; margin-bottom: 20px;">
382 <div style="width: 80px; height: 80px; border-radius: 50%; background: ${avatarColor}; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 24px; margin: 0 auto 15px;">
383 ${contact.profilePic ? `<img src="${contact.profilePic}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">` : initials}
384 </div>
385 <h3 style="margin: 0 0 5px 0; font-size: 18px; color: #262626;">${contact.fullName || contact.username}</h3>
386 <a href="https://www.instagram.com/${contact.username}" target="_blank" style="color: #667eea; text-decoration: none; font-size: 14px;">@${contact.username}</a>
387 </div>
388
389 ${isOverdue ? `
390 <div style="background: #fff3cd; border: 1px solid #ffc107; padding: 12px; border-radius: 8px; margin-bottom: 15px; text-align: center;">
391 <div style="font-size: 14px; color: #856404; font-weight: 600;">⏰ Follow-up Reminder</div>
392 <div style="font-size: 13px; color: #856404; margin-top: 5px;">Last contact: ${daysSinceContact} days ago</div>
393 </div>
394 ` : ''}
395
396 <div style="margin-bottom: 15px;">
397 <label style="display: block; font-size: 12px; color: #8e8e8e; margin-bottom: 8px; font-weight: 600;">CONTACT TRACKING</label>
398 <div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 10px;">
399 <div style="font-size: 13px; color: #262626; margin-bottom: 8px;">
400 ${lastContactDate ? `Last contacted: ${lastContactDate.toLocaleDateString()} (${daysSinceContact} days ago)` : 'No contact recorded yet'}
401 </div>
402 <div style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px;">
403 <label style="font-size: 13px; color: #262626;">Remind me every:</label>
404 <select id="crm-reminder-interval" style="padding: 6px; border: 1px solid #dbdbdb; border-radius: 6px; font-size: 13px;">
405 <option value="1" ${reminderInterval === 1 ? 'selected' : ''}>1 day</option>
406 <option value="2" ${reminderInterval === 2 ? 'selected' : ''}>2 days</option>
407 <option value="3" ${reminderInterval === 3 ? 'selected' : ''}>3 days</option>
408 <option value="5" ${reminderInterval === 5 ? 'selected' : ''}>5 days</option>
409 <option value="7" ${reminderInterval === 7 ? 'selected' : ''}>7 days</option>
410 <option value="14" ${reminderInterval === 14 ? 'selected' : ''}>14 days</option>
411 <option value="30" ${reminderInterval === 30 ? 'selected' : ''}>30 days</option>
412 </select>
413 </div>
414 </div>
415 <button id="crm-mark-contacted" style="width: 100%; padding: 10px; background: #43e97b; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer;">✓ Mark as Contacted Today</button>
416 </div>
417
418 ${contact.bio ? `
419 <div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 15px;">
420 <div style="font-size: 12px; color: #8e8e8e; margin-bottom: 5px; font-weight: 600;">BIO</div>
421 <div style="font-size: 14px; color: #262626;">${contact.bio}</div>
422 </div>
423 ` : ''}
424
425 ${contact.followers || contact.following ? `
426 <div style="display: flex; gap: 10px; margin-bottom: 15px;">
427 ${contact.followers ? `
428 <div style="flex: 1; background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
429 <div style="font-size: 16px; font-weight: 600; color: #262626;">${contact.followers}</div>
430 <div style="font-size: 12px; color: #8e8e8e;">Followers</div>
431 </div>
432 ` : ''}
433 ${contact.following ? `
434 <div style="flex: 1; background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
435 <div style="font-size: 16px; font-weight: 600; color: #262626;">${contact.following}</div>
436 <div style="font-size: 12px; color: #8e8e8e;">Following</div>
437 </div>
438 ` : ''}
439 </div>
440 ` : ''}
441
442 <div style="margin-bottom: 15px;">
443 <label style="display: block; font-size: 12px; color: #8e8e8e; margin-bottom: 5px; font-weight: 600;">TAGS</label>
444 <div id="crm-tags-container" style="display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 8px;">
445 ${contact.tags && contact.tags.length > 0 ? contact.tags.map(tag => `
446 <span class="crm-tag" data-tag="${tag}" style="background: #e0e7ff; color: #4c51bf; padding: 5px 10px; border-radius: 15px; font-size: 12px; display: flex; align-items: center; gap: 5px;">
447 ${tag}
448 <span class="crm-tag-remove" style="cursor: pointer; font-weight: bold;">×</span>
449 </span>
450 `).join('') : '<span style="font-size: 13px; color: #8e8e8e;">No tags</span>'}
451 </div>
452 <input type="text" id="crm-add-tag" placeholder="Add tag and press Enter" style="width: 100%; padding: 8px; border: 1px solid #dbdbdb; border-radius: 6px; font-size: 13px; box-sizing: border-box;">
453 </div>
454
455 <div style="margin-bottom: 15px;">
456 <label style="display: block; font-size: 12px; color: #8e8e8e; margin-bottom: 5px; font-weight: 600;">NOTES</label>
457 <textarea id="crm-notes" style="width: 100%; min-height: 100px; padding: 10px; border: 1px solid #dbdbdb; border-radius: 8px; font-size: 14px; font-family: inherit; resize: vertical; box-sizing: border-box;">${contact.notes || ''}</textarea>
458 <button id="crm-save-notes" style="margin-top: 8px; width: 100%; padding: 10px; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer;">Save Notes</button>
459 </div>
460
461 <div style="font-size: 11px; color: #8e8e8e; margin-bottom: 15px;">
462 Added: ${new Date(contact.dateAdded).toLocaleDateString()}<br>
463 Last updated: ${new Date(contact.lastUpdated).toLocaleDateString()}
464 </div>
465
466 <button id="crm-delete-contact" style="width: 100%; padding: 10px; background: #ed4956; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer;">Delete Contact</button>
467 </div>
468 `;
469
470 // Event listeners
471 content.querySelector('#crm-back-btn').addEventListener('click', () => this.renderContactList());
472
473 content.querySelector('#crm-mark-contacted').addEventListener('click', async () => {
474 await this.dataManager.markAsContacted(contact.id);
475 this.updateBadge();
476 this.showContactDetail(contact.id);
477 });
478
479 content.querySelector('#crm-reminder-interval').addEventListener('change', async (e) => {
480 const interval = parseInt(e.target.value);
481 await this.dataManager.updateContact(contact.id, { reminderInterval: interval });
482 this.selectedContact.reminderInterval = interval;
483 this.updateBadge();
484 });
485
486 content.querySelector('#crm-save-notes').addEventListener('click', async () => {
487 const notes = content.querySelector('#crm-notes').value;
488 await this.dataManager.updateContact(contact.id, { notes });
489 this.selectedContact.notes = notes;
490 alert('Notes saved!');
491 });
492
493 content.querySelector('#crm-delete-contact').addEventListener('click', async () => {
494 if (confirm(`Delete ${contact.username} from your CRM?`)) {
495 await this.dataManager.deleteContact(contact.id);
496 this.updateBadge();
497 this.renderContactList();
498 }
499 });
500
501 // Tag management
502 const tagInput = content.querySelector('#crm-add-tag');
503 tagInput.addEventListener('keypress', async (e) => {
504 if (e.key === 'Enter' && tagInput.value.trim()) {
505 const newTag = tagInput.value.trim();
506 const updatedTags = [...(contact.tags || []), newTag];
507 await this.dataManager.updateContact(contact.id, { tags: updatedTags });
508 this.selectedContact.tags = updatedTags;
509 this.showContactDetail(contact.id);
510 }
511 });
512
513 // Tag removal
514 content.querySelectorAll('.crm-tag-remove').forEach(removeBtn => {
515 removeBtn.addEventListener('click', async (e) => {
516 const tagElement = e.target.closest('.crm-tag');
517 const tagToRemove = tagElement.getAttribute('data-tag');
518 const updatedTags = contact.tags.filter(t => t !== tagToRemove);
519 await this.dataManager.updateContact(contact.id, { tags: updatedTags });
520 this.selectedContact.tags = updatedTags;
521 this.showContactDetail(contact.id);
522 });
523 });
524 }
525
526 handleSearch(query) {
527 if (!query.trim()) {
528 this.renderContactList();
529 return;
530 }
531
532 const results = this.dataManager.searchContacts(query);
533 const content = this.panel.querySelector('#crm-content');
534
535 if (results.length === 0) {
536 content.innerHTML = `
537 <div style="text-align: center; padding: 40px 20px; color: #8e8e8e;">
538 <div style="font-size: 48px; margin-bottom: 15px;">🔍</div>
539 <p style="margin: 0; font-size: 16px; font-weight: 500;">No results found</p>
540 <p style="margin: 10px 0 0 0; font-size: 14px;">Try a different search term</p>
541 </div>
542 `;
543 return;
544 }
545
546 let html = '<div style="display: flex; flex-direction: column; gap: 10px;">';
547
548 results.forEach(contact => {
549 const initials = contact.fullName ? contact.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) : contact.username.slice(0, 2).toUpperCase();
550 const avatarColor = this.getColorFromString(contact.username);
551
552 html += `
553 <div class="crm-contact-card" data-id="${contact.id}" style="background: white; border: 1px solid #dbdbdb; border-radius: 12px; padding: 15px; cursor: pointer; transition: all 0.2s ease;">
554 <div style="display: flex; align-items: center; gap: 12px;">
555 <div style="width: 50px; height: 50px; border-radius: 50%; background: ${avatarColor}; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 16px; flex-shrink: 0;">
556 ${contact.profilePic ? `<img src="${contact.profilePic}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">` : initials}
557 </div>
558 <div style="flex: 1; min-width: 0;">
559 <div style="font-weight: 600; font-size: 14px; color: #262626; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
560 ${contact.fullName || contact.username}
561 </div>
562 <div style="font-size: 13px; color: #8e8e8e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
563 @${contact.username}
564 </div>
565 </div>
566 </div>
567 </div>
568 `;
569 });
570
571 html += '</div>';
572 content.innerHTML = html;
573
574 // Add click listeners
575 content.querySelectorAll('.crm-contact-card').forEach(card => {
576 card.addEventListener('mouseenter', () => {
577 card.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
578 card.style.transform = 'translateY(-2px)';
579 });
580 card.addEventListener('mouseleave', () => {
581 card.style.boxShadow = 'none';
582 card.style.transform = 'translateY(0)';
583 });
584 card.addEventListener('click', () => {
585 const contactId = card.getAttribute('data-id');
586 this.showContactDetail(contactId);
587 });
588 });
589 }
590
591 getColorFromString(str) {
592 const colors = [
593 '#667eea', '#764ba2', '#f093fb', '#4facfe',
594 '#43e97b', '#fa709a', '#fee140', '#30cfd0',
595 '#a8edea', '#fed6e3', '#c471f5', '#fa71cd'
596 ];
597 let hash = 0;
598 for (let i = 0; i < str.length; i++) {
599 hash = str.charCodeAt(i) + ((hash << 5) - hash);
600 }
601 return colors[Math.abs(hash) % colors.length];
602 }
603 }
604
605 // Profile Scraper
606 class ProfileScraper {
607 async scrapeCurrentProfile() {
608 console.log('Scraping profile data...');
609
610 // Get username from URL
611 const urlMatch = window.location.pathname.match(/^\/([^\/]+)\/?$/);
612 if (!urlMatch) {
613 console.log('Not on a profile page');
614 return null;
615 }
616
617 const username = urlMatch[1];
618
619 // Wait for profile data to load
620 await new Promise(resolve => setTimeout(resolve, 2000));
621
622 const profileData = {
623 username: username,
624 fullName: '',
625 bio: '',
626 followers: '',
627 following: '',
628 profilePic: '',
629 profileUrl: window.location.href
630 };
631
632 // Try to get full name
633 const nameElement = document.querySelector('header section h2, header section span[dir="auto"]');
634 if (nameElement) {
635 profileData.fullName = nameElement.textContent.trim();
636 }
637
638 // Try to get bio
639 const bioElement = document.querySelector('header section span[style*="line-height"], header section div[style*="line-height"] span');
640 if (bioElement) {
641 profileData.bio = bioElement.textContent.trim();
642 }
643
644 // Try to get follower/following counts
645 const statsElements = document.querySelectorAll('header section ul li span[title], header section ul li span span');
646 if (statsElements.length >= 2) {
647 profileData.followers = statsElements[0].textContent.trim();
648 profileData.following = statsElements[1].textContent.trim();
649 }
650
651 // Try to get profile picture
652 const profilePicElement = document.querySelector('header img[alt*="profile picture"], header img[alt*="' + username + '"]');
653 if (profilePicElement) {
654 profileData.profilePic = profilePicElement.src;
655 }
656
657 console.log('Scraped profile data:', profileData);
658 return profileData;
659 }
660
661 addSaveButton(dataManager, uiManager) {
662 // Remove existing button if any
663 const existingBtn = document.querySelector('#crm-save-profile-btn');
664 if (existingBtn) existingBtn.remove();
665
666 // Check if we're on a profile page
667 const urlMatch = window.location.pathname.match(/^\/([^\/]+)\/?$/);
668 if (!urlMatch) return;
669
670 // Wait for header to load
671 setTimeout(() => {
672 const header = document.querySelector('header section');
673 if (!header) return;
674
675 const saveBtn = document.createElement('button');
676 saveBtn.id = 'crm-save-profile-btn';
677 saveBtn.innerHTML = '💾 Save to CRM';
678 saveBtn.style.cssText = `
679 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
680 color: white;
681 border: none;
682 padding: 8px 16px;
683 border-radius: 8px;
684 font-size: 14px;
685 font-weight: 600;
686 cursor: pointer;
687 margin-left: 8px;
688 transition: transform 0.2s ease;
689 `;
690
691 saveBtn.addEventListener('mouseenter', () => {
692 saveBtn.style.transform = 'scale(1.05)';
693 });
694
695 saveBtn.addEventListener('mouseleave', () => {
696 saveBtn.style.transform = 'scale(1)';
697 });
698
699 saveBtn.addEventListener('click', async () => {
700 saveBtn.disabled = true;
701 saveBtn.innerHTML = '⏳ Saving...';
702
703 const profileData = await this.scrapeCurrentProfile();
704 if (profileData) {
705 await dataManager.addContact(profileData);
706 saveBtn.innerHTML = '✅ Saved!';
707 setTimeout(() => {
708 saveBtn.innerHTML = '💾 Save to CRM';
709 saveBtn.disabled = false;
710 }, 2000);
711 } else {
712 saveBtn.innerHTML = '❌ Failed';
713 setTimeout(() => {
714 saveBtn.innerHTML = '💾 Save to CRM';
715 saveBtn.disabled = false;
716 }, 2000);
717 }
718 });
719
720 // Find a good place to insert the button
721 const buttonContainer = header.querySelector('div[style*="flex-direction: row"]');
722 if (buttonContainer) {
723 buttonContainer.appendChild(saveBtn);
724 } else {
725 header.appendChild(saveBtn);
726 }
727
728 console.log('Save button added to profile');
729 }, 2000);
730 }
731 }
732
733 // Main initialization
734 async function init() {
735 console.log('Instagram CRM Manager initializing...');
736
737 const dataManager = new CRMDataManager();
738 await dataManager.init();
739
740 const uiManager = new CRMUIManager(dataManager);
741 uiManager.createPanel();
742 uiManager.createToggleButton();
743
744 const scraper = new ProfileScraper();
745
746 // Add save button on profile pages
747 if (window.location.pathname.match(/^\/[^\/]+\/?$/)) {
748 scraper.addSaveButton(dataManager, uiManager);
749 }
750
751 // Watch for navigation changes
752 let lastUrl = window.location.href;
753 const observer = new MutationObserver(debounce(() => {
754 if (window.location.href !== lastUrl) {
755 lastUrl = window.location.href;
756 console.log('URL changed:', lastUrl);
757
758 if (window.location.pathname.match(/^\/[^\/]+\/?$/)) {
759 scraper.addSaveButton(dataManager, uiManager);
760 }
761 }
762 }, 500));
763
764 observer.observe(document.body, {
765 childList: true,
766 subtree: true
767 });
768
769 console.log('Instagram CRM Manager initialized successfully!');
770 }
771
772 // Start when page is ready
773 if (document.readyState === 'loading') {
774 document.addEventListener('DOMContentLoaded', init);
775 } else {
776 init();
777 }
778})();