@rashidazarang/airtable-mcp 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/package.json +10 -1
  2. package/.github/ISSUE_TEMPLATE/bug-report.yml +0 -173
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  4. package/.github/ISSUE_TEMPLATE/custom.md +0 -10
  5. package/.github/ISSUE_TEMPLATE/feature-request.yml +0 -209
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  7. package/.github/ISSUE_TEMPLATE/security-report.yml +0 -216
  8. package/.github/pull_request_template.md +0 -245
  9. package/.github/workflows/ci-cd.yml +0 -408
  10. package/.github/workflows/security-audit.yml +0 -316
  11. package/API_DOCUMENTATION.md +0 -897
  12. package/CAPABILITY_REPORT.md +0 -118
  13. package/CLAUDE_INTEGRATION.md +0 -96
  14. package/CODE_OF_CONDUCT.md +0 -181
  15. package/CONTRIBUTING.md +0 -81
  16. package/DEVELOPMENT.md +0 -190
  17. package/Dockerfile +0 -39
  18. package/Dockerfile.node +0 -20
  19. package/Dockerfile.production +0 -127
  20. package/IMPROVEMENT_PROPOSAL.md +0 -371
  21. package/INSTALLATION.md +0 -183
  22. package/ISSUE_RESPONSES.md +0 -171
  23. package/MCP_REVIEW_SUMMARY.md +0 -142
  24. package/QUICK_START.md +0 -60
  25. package/RELEASE_NOTES_v1.2.0.md +0 -50
  26. package/RELEASE_NOTES_v1.2.1.md +0 -40
  27. package/RELEASE_NOTES_v1.2.2.md +0 -48
  28. package/RELEASE_NOTES_v1.2.3.md +0 -105
  29. package/RELEASE_NOTES_v1.2.4.md +0 -60
  30. package/RELEASE_NOTES_v1.4.0.md +0 -104
  31. package/RELEASE_NOTES_v1.5.0.md +0 -185
  32. package/RELEASE_NOTES_v1.6.0.md +0 -248
  33. package/SECURITY_NOTICE.md +0 -40
  34. package/airtable-clipper/CHANGELOG.md +0 -198
  35. package/airtable-clipper/CHROME_STORE_SUBMISSION.md +0 -343
  36. package/airtable-clipper/LAUNCH_STRATEGY.md +0 -495
  37. package/airtable-clipper/LICENSE +0 -21
  38. package/airtable-clipper/OAUTH_SETUP.md +0 -51
  39. package/airtable-clipper/PRIVACY_POLICY.md +0 -187
  40. package/airtable-clipper/README.md +0 -575
  41. package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +0 -273
  42. package/airtable-clipper/build.sh +0 -85
  43. package/airtable-clipper/docs/QUICK_START.md +0 -99
  44. package/airtable-clipper/docs/SETUP.md +0 -291
  45. package/airtable-clipper/extension/background.js +0 -337
  46. package/airtable-clipper/extension/base-setup.html +0 -324
  47. package/airtable-clipper/extension/base-setup.js +0 -471
  48. package/airtable-clipper/extension/content.js +0 -771
  49. package/airtable-clipper/extension/icons/README.md +0 -69
  50. package/airtable-clipper/extension/icons/icon-16.png +0 -3
  51. package/airtable-clipper/extension/manifest.json +0 -73
  52. package/airtable-clipper/extension/popup.html +0 -144
  53. package/airtable-clipper/extension/popup.js +0 -475
  54. package/airtable-clipper/extension/styles/content.css +0 -229
  55. package/airtable-clipper/extension/styles/popup.css +0 -477
  56. package/airtable-clipper/privacy-policy.md +0 -63
  57. package/airtable-clipper/releases/v1.0.0/background.js +0 -337
  58. package/airtable-clipper/releases/v1.0.0/base-setup.html +0 -324
  59. package/airtable-clipper/releases/v1.0.0/base-setup.js +0 -471
  60. package/airtable-clipper/releases/v1.0.0/content.js +0 -771
  61. package/airtable-clipper/releases/v1.0.0/icons/README.md +0 -69
  62. package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +0 -2
  63. package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +0 -3
  64. package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +0 -2
  65. package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +0 -2
  66. package/airtable-clipper/releases/v1.0.0/manifest.json +0 -73
  67. package/airtable-clipper/releases/v1.0.0/popup.html +0 -144
  68. package/airtable-clipper/releases/v1.0.0/popup.js +0 -475
  69. package/airtable-clipper/releases/v1.0.0/sidepanel.html +0 -25
  70. package/airtable-clipper/releases/v1.0.0/styles/content.css +0 -229
  71. package/airtable-clipper/releases/v1.0.0/styles/popup.css +0 -477
  72. package/airtable-clipper/releases/v1.0.1/background.js +0 -337
  73. package/airtable-clipper/releases/v1.0.1/base-setup.html +0 -324
  74. package/airtable-clipper/releases/v1.0.1/base-setup.js +0 -471
  75. package/airtable-clipper/releases/v1.0.1/content.js +0 -771
  76. package/airtable-clipper/releases/v1.0.1/icons/README.md +0 -69
  77. package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +0 -2
  78. package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +0 -3
  79. package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +0 -2
  80. package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +0 -2
  81. package/airtable-clipper/releases/v1.0.1/manifest.json +0 -70
  82. package/airtable-clipper/releases/v1.0.1/popup.html +0 -157
  83. package/airtable-clipper/releases/v1.0.1/popup.js +0 -562
  84. package/airtable-clipper/releases/v1.0.1/sidepanel.html +0 -25
  85. package/airtable-clipper/releases/v1.0.1/styles/content.css +0 -229
  86. package/airtable-clipper/releases/v1.0.1/styles/popup.css +0 -647
  87. package/airtable-clipper/releases/v1.0.2/background.js +0 -337
  88. package/airtable-clipper/releases/v1.0.2/base-setup.html +0 -324
  89. package/airtable-clipper/releases/v1.0.2/base-setup.js +0 -471
  90. package/airtable-clipper/releases/v1.0.2/content.js +0 -771
  91. package/airtable-clipper/releases/v1.0.2/icons/README.md +0 -69
  92. package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +0 -2
  93. package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +0 -3
  94. package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +0 -2
  95. package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +0 -2
  96. package/airtable-clipper/releases/v1.0.2/manifest.json +0 -62
  97. package/airtable-clipper/releases/v1.0.2/popup.html +0 -157
  98. package/airtable-clipper/releases/v1.0.2/popup.js +0 -567
  99. package/airtable-clipper/releases/v1.0.2/sidepanel.html +0 -25
  100. package/airtable-clipper/releases/v1.0.2/styles/content.css +0 -229
  101. package/airtable-clipper/releases/v1.0.2/styles/popup.css +0 -647
  102. package/airtable-clipper/terms-of-service.md +0 -124
  103. package/airtable-clipper/test-credentials.md +0 -61
  104. package/airtable-clipper/test-extension/background.js +0 -337
  105. package/airtable-clipper/test-extension/base-setup.html +0 -324
  106. package/airtable-clipper/test-extension/base-setup.js +0 -471
  107. package/airtable-clipper/test-extension/content.js +0 -873
  108. package/airtable-clipper/test-extension/icons/README.md +0 -69
  109. package/airtable-clipper/test-extension/icons/icon-128.png +0 -2
  110. package/airtable-clipper/test-extension/icons/icon-16.png +0 -3
  111. package/airtable-clipper/test-extension/icons/icon-32.png +0 -2
  112. package/airtable-clipper/test-extension/icons/icon-48.png +0 -2
  113. package/airtable-clipper/test-extension/manifest.json +0 -72
  114. package/airtable-clipper/test-extension/popup.html +0 -274
  115. package/airtable-clipper/test-extension/popup.js +0 -729
  116. package/airtable-clipper/test-extension/sidepanel.html +0 -25
  117. package/airtable-clipper/test-extension/styles/content.css +0 -229
  118. package/airtable-clipper/test-extension/styles/popup.css +0 -794
  119. package/airtable_mcp/__init__.py +0 -5
  120. package/airtable_mcp/src/server.py +0 -329
  121. package/airtable_mcp_v2.js +0 -1505
  122. package/airtable_mcp_v2_oauth.js +0 -1048
  123. package/airtable_mcp_v3_advanced.js +0 -1161
  124. package/cleanup.sh +0 -71
  125. package/docker-compose.production.yml +0 -366
  126. package/helm/airtable-mcp/Chart.yaml +0 -122
  127. package/helm/airtable-mcp/values.yaml +0 -538
  128. package/index.js +0 -179
  129. package/inspector.py +0 -148
  130. package/inspector_server.py +0 -337
  131. package/k8s/deployment.yaml +0 -402
  132. package/k8s/namespace.yaml +0 -108
  133. package/k8s/service.yaml +0 -194
  134. package/monitoring/alerts.yml +0 -289
  135. package/monitoring/prometheus.yml +0 -224
  136. package/publish-steps.txt +0 -27
  137. package/quick_test.sh +0 -30
  138. package/requirements.txt +0 -10
  139. package/setup.py +0 -29
  140. package/simple_airtable_server.py +0 -151
  141. package/smithery.yaml +0 -45
  142. package/test_all_features.sh +0 -146
  143. package/test_all_operations.sh +0 -120
  144. package/test_client.py +0 -70
  145. package/test_enhanced_features.js +0 -389
  146. package/test_mcp_comprehensive.js +0 -163
  147. package/test_mock_server.js +0 -180
  148. package/test_v1.4.0_final.sh +0 -131
  149. package/test_v1.5.0_comprehensive.sh +0 -96
  150. package/test_v1.5.0_final.sh +0 -224
  151. package/test_v1.6.0_comprehensive.sh +0 -187
  152. package/test_webhooks.sh +0 -105
@@ -1,771 +0,0 @@
1
- // Airtable Clipper Content Script
2
- // Runs on all web pages to provide extraction and interaction capabilities
3
-
4
- import { LinkedInScraper } from './lib/linkedin-scraper.js';
5
-
6
- class ContentScriptController {
7
- constructor() {
8
- this.isLinkedInPage = window.location.hostname.includes('linkedin.com');
9
- this.bulkModeActive = false;
10
- this.selectedElements = new Set();
11
- this.floatingButton = null;
12
- this.init();
13
- }
14
-
15
- init() {
16
- // Wait for page to load
17
- if (document.readyState === 'loading') {
18
- document.addEventListener('DOMContentLoaded', () => this.setup());
19
- } else {
20
- this.setup();
21
- }
22
- }
23
-
24
- setup() {
25
- // Listen for messages from popup and background
26
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
27
- this.handleMessage(message, sender, sendResponse);
28
- return true; // Keep message channel open for async responses
29
- });
30
-
31
- // Create floating action button if appropriate
32
- if (this.shouldShowFloatingButton()) {
33
- this.createFloatingButton();
34
- }
35
-
36
- // LinkedIn-specific setup
37
- if (this.isLinkedInPage) {
38
- this.setupLinkedInFeatures();
39
- }
40
-
41
- // Setup general extraction features
42
- this.setupGeneralFeatures();
43
- }
44
-
45
- async handleMessage(message, sender, sendResponse) {
46
- try {
47
- switch (message.action) {
48
- case 'extractPageData':
49
- const pageData = await this.extractPageData(message.type);
50
- sendResponse({ success: true, data: pageData });
51
- break;
52
-
53
- case 'extractLinkedInProfile':
54
- const profileData = await this.extractLinkedInProfile();
55
- sendResponse({ success: true, data: profileData });
56
- break;
57
-
58
- case 'enterBulkMode':
59
- this.enterBulkMode();
60
- sendResponse({ success: true });
61
- break;
62
-
63
- case 'exitBulkMode':
64
- this.exitBulkMode();
65
- sendResponse({ success: true });
66
- break;
67
-
68
- case 'triggerSave':
69
- await this.triggerSave(message.data);
70
- sendResponse({ success: true });
71
- break;
72
-
73
- default:
74
- sendResponse({ success: false, error: 'Unknown action' });
75
- }
76
- } catch (error) {
77
- console.error('Content script error:', error);
78
- sendResponse({ success: false, error: error.message });
79
- }
80
- }
81
-
82
- shouldShowFloatingButton() {
83
- // Show floating button on supported sites
84
- const supportedSites = [
85
- 'linkedin.com',
86
- 'github.com',
87
- 'stackoverflow.com',
88
- 'medium.com',
89
- 'dev.to'
90
- ];
91
-
92
- return supportedSites.some(site => window.location.hostname.includes(site));
93
- }
94
-
95
- createFloatingButton() {
96
- // Remove existing button if any
97
- if (this.floatingButton) {
98
- this.floatingButton.remove();
99
- }
100
-
101
- // Create floating action button
102
- this.floatingButton = document.createElement('div');
103
- this.floatingButton.id = 'airtable-clipper-fab';
104
- this.floatingButton.innerHTML = `
105
- <div class="fab-main" title="Save to Airtable">
106
- 📎
107
- </div>
108
- `;
109
-
110
- // Add styles
111
- this.floatingButton.style.cssText = `
112
- position: fixed;
113
- bottom: 20px;
114
- right: 20px;
115
- z-index: 10000;
116
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
117
- `;
118
-
119
- // Style the main button
120
- const fabMain = this.floatingButton.querySelector('.fab-main');
121
- fabMain.style.cssText = `
122
- width: 56px;
123
- height: 56px;
124
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
125
- border-radius: 50%;
126
- display: flex;
127
- align-items: center;
128
- justify-content: center;
129
- color: white;
130
- font-size: 20px;
131
- cursor: pointer;
132
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
133
- transition: all 0.3s ease;
134
- user-select: none;
135
- `;
136
-
137
- // Add hover effects
138
- fabMain.addEventListener('mouseenter', () => {
139
- fabMain.style.transform = 'scale(1.1)';
140
- fabMain.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
141
- });
142
-
143
- fabMain.addEventListener('mouseleave', () => {
144
- fabMain.style.transform = 'scale(1)';
145
- fabMain.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.4)';
146
- });
147
-
148
- // Add click handler
149
- fabMain.addEventListener('click', (e) => {
150
- e.preventDefault();
151
- e.stopPropagation();
152
- this.handleFloatingButtonClick();
153
- });
154
-
155
- // Add to page
156
- document.body.appendChild(this.floatingButton);
157
- }
158
-
159
- async handleFloatingButtonClick() {
160
- try {
161
- if (this.isLinkedInPage && window.location.pathname.includes('/in/')) {
162
- // LinkedIn profile page - extract profile
163
- await this.triggerSave({ type: 'linkedin' });
164
- } else {
165
- // General page - extract page data
166
- await this.triggerSave({ type: 'general' });
167
- }
168
- } catch (error) {
169
- console.error('Floating button click failed:', error);
170
- this.showNotification('Failed to save content', 'error');
171
- }
172
- }
173
-
174
- async triggerSave(data) {
175
- try {
176
- let extractedData;
177
-
178
- if (data.type === 'linkedin') {
179
- extractedData = await this.extractLinkedInProfile();
180
- } else {
181
- extractedData = await this.extractPageData(data.type);
182
- }
183
-
184
- // Send to background script for saving
185
- const response = await chrome.runtime.sendMessage({
186
- action: 'saveToAirtable',
187
- data: {
188
- ...data,
189
- ...extractedData,
190
- timestamp: new Date().toISOString()
191
- }
192
- });
193
-
194
- if (response && response.success) {
195
- this.showNotification('Saved to Airtable!', 'success');
196
- } else {
197
- throw new Error(response?.error || 'Failed to save');
198
- }
199
- } catch (error) {
200
- console.error('Save failed:', error);
201
- this.showNotification(`Save failed: ${error.message}`, 'error');
202
- }
203
- }
204
-
205
- async extractPageData(type = 'general') {
206
- const data = {
207
- url: window.location.href,
208
- title: document.title,
209
- domain: window.location.hostname,
210
- extractedAt: new Date().toISOString()
211
- };
212
-
213
- // Extract meta information
214
- const description = document.querySelector('meta[name="description"]');
215
- if (description) {
216
- data.description = description.content;
217
- }
218
-
219
- const keywords = document.querySelector('meta[name="keywords"]');
220
- if (keywords) {
221
- data.keywords = keywords.content;
222
- }
223
-
224
- // Extract main content based on page type
225
- if (type === 'selection' && window.getSelection().toString()) {
226
- data.content = window.getSelection().toString();
227
- data.type = 'Text Selection';
228
- } else {
229
- data.content = this.extractMainContent();
230
- data.type = 'Web Page';
231
- }
232
-
233
- // Site-specific extractions
234
- if (window.location.hostname.includes('github.com')) {
235
- data.github = this.extractGitHubData();
236
- } else if (window.location.hostname.includes('stackoverflow.com')) {
237
- data.stackoverflow = this.extractStackOverflowData();
238
- } else if (window.location.hostname.includes('medium.com')) {
239
- data.medium = this.extractMediumData();
240
- }
241
-
242
- return data;
243
- }
244
-
245
- extractMainContent() {
246
- // Try different selectors for main content
247
- const contentSelectors = [
248
- 'main',
249
- 'article',
250
- '[role="main"]',
251
- '.content',
252
- '.post-content',
253
- '.entry-content',
254
- '#content',
255
- '.main-content'
256
- ];
257
-
258
- for (const selector of contentSelectors) {
259
- const element = document.querySelector(selector);
260
- if (element) {
261
- return this.getTextContent(element).substring(0, 5000); // Limit to 5000 chars
262
- }
263
- }
264
-
265
- // Fallback: get text from body, excluding navigation and footer
266
- const body = document.body.cloneNode(true);
267
-
268
- // Remove common navigation and footer elements
269
- const elementsToRemove = ['nav', 'header', 'footer', '.nav', '.navigation', '.menu', '.sidebar'];
270
- elementsToRemove.forEach(selector => {
271
- const elements = body.querySelectorAll(selector);
272
- elements.forEach(el => el.remove());
273
- });
274
-
275
- return this.getTextContent(body).substring(0, 5000);
276
- }
277
-
278
- getTextContent(element) {
279
- // Get clean text content from element
280
- const text = element.textContent || element.innerText || '';
281
- return text.replace(/\s+/g, ' ').trim();
282
- }
283
-
284
- async extractLinkedInProfile() {
285
- if (!this.isLinkedInPage) {
286
- throw new Error('Not on LinkedIn');
287
- }
288
-
289
- const scraper = new LinkedInScraper();
290
- const profileData = await scraper.extractProfile();
291
-
292
- return {
293
- type: 'linkedin',
294
- ...profileData
295
- };
296
- }
297
-
298
- extractGitHubData() {
299
- const data = {};
300
-
301
- // Repository name and owner
302
- const repoInfo = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/);
303
- if (repoInfo) {
304
- data.owner = repoInfo[1];
305
- data.repository = repoInfo[2];
306
- }
307
-
308
- // Stars and forks
309
- const stars = document.querySelector('#repo-stars-counter-star');
310
- if (stars) data.stars = stars.textContent.trim();
311
-
312
- const forks = document.querySelector('#repo-network-counter');
313
- if (forks) data.forks = forks.textContent.trim();
314
-
315
- // Description
316
- const description = document.querySelector('[data-pjax="#repo-content-pjax-container"] p');
317
- if (description) data.description = description.textContent.trim();
318
-
319
- // Language
320
- const language = document.querySelector('[data-ga-click*="Language"]');
321
- if (language) data.primaryLanguage = language.textContent.trim();
322
-
323
- return data;
324
- }
325
-
326
- extractStackOverflowData() {
327
- const data = {};
328
-
329
- // Question title
330
- const title = document.querySelector('h1[itemprop="name"]');
331
- if (title) data.questionTitle = title.textContent.trim();
332
-
333
- // Question score
334
- const score = document.querySelector('.js-vote-count');
335
- if (score) data.score = score.textContent.trim();
336
-
337
- // Tags
338
- const tags = Array.from(document.querySelectorAll('.post-tag'))
339
- .map(tag => tag.textContent.trim());
340
- if (tags.length > 0) data.tags = tags;
341
-
342
- // Views
343
- const views = document.querySelector('[title*="viewed"]');
344
- if (views) data.views = views.textContent.trim();
345
-
346
- return data;
347
- }
348
-
349
- extractMediumData() {
350
- const data = {};
351
-
352
- // Author
353
- const author = document.querySelector('[data-testid="authorName"]');
354
- if (author) data.author = author.textContent.trim();
355
-
356
- // Publication date
357
- const date = document.querySelector('[data-testid="storyPublishDate"]');
358
- if (date) data.publishDate = date.textContent.trim();
359
-
360
- // Reading time
361
- const readTime = document.querySelector('[data-testid="storyReadTime"]');
362
- if (readTime) data.readingTime = readTime.textContent.trim();
363
-
364
- // Claps
365
- const claps = document.querySelector('[data-testid="clapCount"]');
366
- if (claps) data.claps = claps.textContent.trim();
367
-
368
- return data;
369
- }
370
-
371
- setupLinkedInFeatures() {
372
- // Auto-save feature for LinkedIn profiles
373
- if (window.location.pathname.includes('/in/')) {
374
- this.checkAutoSaveSettings();
375
- }
376
- }
377
-
378
- async checkAutoSaveSettings() {
379
- try {
380
- const response = await chrome.runtime.sendMessage({
381
- action: 'getSettings'
382
- });
383
-
384
- if (response.success && response.settings.autoSave) {
385
- // Auto-save after a delay to ensure page is loaded
386
- setTimeout(() => {
387
- this.triggerSave({ type: 'linkedin', auto: true });
388
- }, 3000);
389
- }
390
- } catch (error) {
391
- console.error('Failed to check auto-save settings:', error);
392
- }
393
- }
394
-
395
- setupGeneralFeatures() {
396
- // Text selection features
397
- document.addEventListener('mouseup', () => {
398
- const selection = window.getSelection().toString().trim();
399
- if (selection.length > 10) {
400
- this.showSelectionToolbar(selection);
401
- } else {
402
- this.hideSelectionToolbar();
403
- }
404
- });
405
- }
406
-
407
- showSelectionToolbar(selectedText) {
408
- // Remove existing toolbar
409
- this.hideSelectionToolbar();
410
-
411
- const selection = window.getSelection();
412
- const range = selection.getRangeAt(0);
413
- const rect = range.getBoundingClientRect();
414
-
415
- // Create toolbar
416
- const toolbar = document.createElement('div');
417
- toolbar.id = 'airtable-clipper-selection-toolbar';
418
- toolbar.innerHTML = `
419
- <button class="selection-btn" data-action="save">
420
- 📎 Save to Airtable
421
- </button>
422
- `;
423
-
424
- // Style toolbar
425
- toolbar.style.cssText = `
426
- position: fixed;
427
- top: ${rect.top + window.scrollY - 40}px;
428
- left: ${rect.left + window.scrollX}px;
429
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
430
- color: white;
431
- padding: 8px 12px;
432
- border-radius: 6px;
433
- font-size: 12px;
434
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
435
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
436
- z-index: 10001;
437
- user-select: none;
438
- `;
439
-
440
- // Style button
441
- const button = toolbar.querySelector('.selection-btn');
442
- button.style.cssText = `
443
- background: none;
444
- border: none;
445
- color: white;
446
- cursor: pointer;
447
- font-size: 12px;
448
- font-family: inherit;
449
- `;
450
-
451
- // Add click handler
452
- button.addEventListener('click', () => {
453
- this.triggerSave({ type: 'selection', text: selectedText });
454
- this.hideSelectionToolbar();
455
- });
456
-
457
- document.body.appendChild(toolbar);
458
-
459
- // Auto-hide after 5 seconds
460
- setTimeout(() => this.hideSelectionToolbar(), 5000);
461
- }
462
-
463
- hideSelectionToolbar() {
464
- const toolbar = document.getElementById('airtable-clipper-selection-toolbar');
465
- if (toolbar) {
466
- toolbar.remove();
467
- }
468
- }
469
-
470
- enterBulkMode() {
471
- if (this.bulkModeActive) return;
472
-
473
- this.bulkModeActive = true;
474
- this.selectedElements.clear();
475
-
476
- // Add bulk mode overlay
477
- this.createBulkModeOverlay();
478
-
479
- // Add click handlers to selectable elements
480
- this.addBulkModeHandlers();
481
-
482
- this.showNotification('Bulk mode activated. Click items to select them.', 'info');
483
- }
484
-
485
- createBulkModeOverlay() {
486
- const overlay = document.createElement('div');
487
- overlay.id = 'airtable-clipper-bulk-overlay';
488
- overlay.innerHTML = `
489
- <div class="bulk-header">
490
- <span class="bulk-title">📋 Bulk Mode Active</span>
491
- <span class="bulk-counter">0 items selected</span>
492
- <div class="bulk-actions">
493
- <button class="bulk-btn save-selected">Save Selected</button>
494
- <button class="bulk-btn cancel-bulk">Cancel</button>
495
- </div>
496
- </div>
497
- `;
498
-
499
- // Style overlay
500
- overlay.style.cssText = `
501
- position: fixed;
502
- top: 0;
503
- left: 0;
504
- right: 0;
505
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
506
- color: white;
507
- padding: 12px 20px;
508
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
509
- font-size: 14px;
510
- z-index: 10002;
511
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
512
- `;
513
-
514
- // Style header
515
- const header = overlay.querySelector('.bulk-header');
516
- header.style.cssText = `
517
- display: flex;
518
- align-items: center;
519
- gap: 20px;
520
- `;
521
-
522
- // Style buttons
523
- const buttons = overlay.querySelectorAll('.bulk-btn');
524
- buttons.forEach(btn => {
525
- btn.style.cssText = `
526
- background: rgba(255,255,255,0.2);
527
- border: 1px solid rgba(255,255,255,0.3);
528
- color: white;
529
- padding: 6px 12px;
530
- border-radius: 4px;
531
- cursor: pointer;
532
- font-size: 12px;
533
- margin-left: 8px;
534
- `;
535
- });
536
-
537
- // Add event handlers
538
- overlay.querySelector('.save-selected').addEventListener('click', () => {
539
- this.saveBulkSelection();
540
- });
541
-
542
- overlay.querySelector('.cancel-bulk').addEventListener('click', () => {
543
- this.exitBulkMode();
544
- });
545
-
546
- document.body.appendChild(overlay);
547
- document.body.style.paddingTop = '60px'; // Make room for overlay
548
- }
549
-
550
- addBulkModeHandlers() {
551
- // Add handlers for clickable elements based on current site
552
- let selectors = [];
553
-
554
- if (this.isLinkedInPage) {
555
- selectors = [
556
- '.search-result__wrapper',
557
- '.entity-result',
558
- '.reusable-search__result-container'
559
- ];
560
- } else {
561
- selectors = [
562
- 'article',
563
- '.item',
564
- '.card',
565
- '.result',
566
- '.post'
567
- ];
568
- }
569
-
570
- selectors.forEach(selector => {
571
- const elements = document.querySelectorAll(selector);
572
- elements.forEach(element => {
573
- element.addEventListener('click', this.handleBulkElementClick.bind(this));
574
- element.style.cursor = 'pointer';
575
- element.style.transition = 'all 0.2s ease';
576
- });
577
- });
578
- }
579
-
580
- handleBulkElementClick(event) {
581
- event.preventDefault();
582
- event.stopPropagation();
583
-
584
- const element = event.currentTarget;
585
- const isSelected = this.selectedElements.has(element);
586
-
587
- if (isSelected) {
588
- this.selectedElements.delete(element);
589
- element.style.background = '';
590
- element.style.border = '';
591
- } else {
592
- this.selectedElements.add(element);
593
- element.style.background = 'rgba(102, 126, 234, 0.1)';
594
- element.style.border = '2px solid #667eea';
595
- }
596
-
597
- this.updateBulkCounter();
598
- }
599
-
600
- updateBulkCounter() {
601
- const counter = document.querySelector('.bulk-counter');
602
- if (counter) {
603
- const count = this.selectedElements.size;
604
- counter.textContent = `${count} item${count !== 1 ? 's' : ''} selected`;
605
- }
606
- }
607
-
608
- async saveBulkSelection() {
609
- if (this.selectedElements.size === 0) {
610
- this.showNotification('No items selected', 'error');
611
- return;
612
- }
613
-
614
- this.showNotification('Saving selected items...', 'info');
615
-
616
- try {
617
- const items = [];
618
-
619
- for (const element of this.selectedElements) {
620
- const itemData = await this.extractElementData(element);
621
- if (itemData) {
622
- items.push(itemData);
623
- }
624
- }
625
-
626
- // Save all items
627
- const response = await chrome.runtime.sendMessage({
628
- action: 'saveBulkToAirtable',
629
- data: {
630
- items: items,
631
- type: 'bulk',
632
- timestamp: new Date().toISOString()
633
- }
634
- });
635
-
636
- if (response && response.success) {
637
- this.showNotification(`Saved ${items.length} items to Airtable!`, 'success');
638
- this.exitBulkMode();
639
- } else {
640
- throw new Error(response?.error || 'Failed to save bulk items');
641
- }
642
- } catch (error) {
643
- console.error('Bulk save failed:', error);
644
- this.showNotification(`Bulk save failed: ${error.message}`, 'error');
645
- }
646
- }
647
-
648
- async extractElementData(element) {
649
- // Extract data from individual element based on site type
650
- const data = {
651
- type: 'bulk_item',
652
- url: window.location.href,
653
- extractedAt: new Date().toISOString()
654
- };
655
-
656
- if (this.isLinkedInPage) {
657
- // LinkedIn search result
658
- const nameElement = element.querySelector('.entity-result__title-text a, .search-result__result-link');
659
- const titleElement = element.querySelector('.entity-result__primary-subtitle, .subline-level-1');
660
- const companyElement = element.querySelector('.entity-result__secondary-subtitle, .subline-level-2');
661
-
662
- if (nameElement) {
663
- data.name = nameElement.textContent.trim();
664
- data.linkedinUrl = nameElement.href;
665
- }
666
- if (titleElement) data.title = titleElement.textContent.trim();
667
- if (companyElement) data.company = companyElement.textContent.trim();
668
- } else {
669
- // General web page element
670
- const title = element.querySelector('h1, h2, h3, .title, .headline');
671
- const link = element.querySelector('a[href]');
672
-
673
- if (title) data.title = title.textContent.trim();
674
- if (link) data.url = link.href;
675
-
676
- data.content = this.getTextContent(element).substring(0, 1000);
677
- }
678
-
679
- return data;
680
- }
681
-
682
- exitBulkMode() {
683
- if (!this.bulkModeActive) return;
684
-
685
- this.bulkModeActive = false;
686
-
687
- // Remove overlay
688
- const overlay = document.getElementById('airtable-clipper-bulk-overlay');
689
- if (overlay) {
690
- overlay.remove();
691
- document.body.style.paddingTop = '';
692
- }
693
-
694
- // Reset selected elements
695
- this.selectedElements.forEach(element => {
696
- element.style.background = '';
697
- element.style.border = '';
698
- element.style.cursor = '';
699
- });
700
- this.selectedElements.clear();
701
-
702
- this.showNotification('Bulk mode deactivated', 'info');
703
- }
704
-
705
- showNotification(message, type = 'info') {
706
- // Remove existing notification
707
- const existing = document.getElementById('airtable-clipper-notification');
708
- if (existing) existing.remove();
709
-
710
- // Create notification
711
- const notification = document.createElement('div');
712
- notification.id = 'airtable-clipper-notification';
713
- notification.textContent = message;
714
-
715
- // Style based on type
716
- const colors = {
717
- success: '#10b981',
718
- error: '#ef4444',
719
- info: '#3b82f6'
720
- };
721
-
722
- notification.style.cssText = `
723
- position: fixed;
724
- top: 20px;
725
- right: 20px;
726
- background: ${colors[type] || colors.info};
727
- color: white;
728
- padding: 12px 16px;
729
- border-radius: 6px;
730
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
731
- font-size: 14px;
732
- font-weight: 500;
733
- z-index: 10003;
734
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
735
- max-width: 300px;
736
- word-wrap: break-word;
737
- animation: slideInRight 0.3s ease;
738
- `;
739
-
740
- // Add animation keyframes
741
- if (!document.querySelector('#airtable-clipper-animations')) {
742
- const style = document.createElement('style');
743
- style.id = 'airtable-clipper-animations';
744
- style.textContent = `
745
- @keyframes slideInRight {
746
- from {
747
- transform: translateX(100%);
748
- opacity: 0;
749
- }
750
- to {
751
- transform: translateX(0);
752
- opacity: 1;
753
- }
754
- }
755
- `;
756
- document.head.appendChild(style);
757
- }
758
-
759
- document.body.appendChild(notification);
760
-
761
- // Auto-hide after 3 seconds
762
- setTimeout(() => {
763
- if (notification && notification.parentNode) {
764
- notification.remove();
765
- }
766
- }, 3000);
767
- }
768
- }
769
-
770
- // Initialize content script
771
- new ContentScriptController();