FetLife New England Female User Search

Search for female users in the New England area on FetLife

Size

15.8 KB

Version

1.0.1

Created

Nov 23, 2025

Updated

3 months ago

1// ==UserScript==
2// @name		FetLife New England Female User Search
3// @description		Search for female users in the New England area on FetLife
4// @version		1.0.1
5// @match		https://*.fetlife.com/*
6// @icon		https://fetlife.com/favicons/icon.svg
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlhttpRequest
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    // New England states
15    const NEW_ENGLAND_STATES = ['Connecticut', 'Maine', 'Massachusetts', 'New Hampshire', 'Rhode Island', 'Vermont', 'CT', 'ME', 'MA', 'NH', 'RI', 'VT'];
16    
17    // Configuration
18    const CONFIG = {
19        searchDelay: 2000, // Delay between profile checks to avoid rate limiting
20        maxProfiles: 100, // Maximum number of profiles to check
21        startUserId: 1, // Starting user ID
22    };
23
24    // Debounce function
25    function debounce(func, wait) {
26        let timeout;
27        return function executedFunction(...args) {
28            const later = () => {
29                clearTimeout(timeout);
30                func(...args);
31            };
32            clearTimeout(timeout);
33            timeout = setTimeout(later, wait);
34        };
35    }
36
37    // Check if location contains New England state
38    function isNewEnglandLocation(location) {
39        if (!location) return false;
40        const locationUpper = location.toUpperCase();
41        return NEW_ENGLAND_STATES.some(state => locationUpper.includes(state.toUpperCase()));
42    }
43
44    // Fetch user profile data
45    async function fetchUserProfile(userId) {
46        return new Promise((resolve, reject) => {
47            GM.xmlhttpRequest({
48                method: 'GET',
49                url: `https://fetlife.com/users/${userId}`,
50                onload: function(response) {
51                    if (response.status === 200) {
52                        resolve(response.responseText);
53                    } else if (response.status === 404) {
54                        resolve(null); // User doesn't exist
55                    } else {
56                        reject(new Error(`HTTP ${response.status}`));
57                    }
58                },
59                onerror: function(error) {
60                    reject(error);
61                }
62            });
63        });
64    }
65
66    // Parse user profile from HTML
67    function parseUserProfile(html, userId) {
68        const parser = new DOMParser();
69        const doc = parser.parseFromString(html, 'text/html');
70        
71        // Extract user information
72        const nickname = doc.querySelector('h2.text-2xl')?.textContent.trim() || 'Unknown';
73        
74        // Find gender/sex information
75        let gender = null;
76        const infoItems = doc.querySelectorAll('em.text-gray-400');
77        infoItems.forEach(item => {
78            const text = item.textContent.trim();
79            if (text.includes('♀') || text.toLowerCase().includes('female') || text.toLowerCase().includes('woman')) {
80                gender = 'Female';
81            }
82        });
83        
84        // Find location
85        let location = null;
86        const locationElement = doc.querySelector('a[href*="/countries/"]');
87        if (locationElement) {
88            location = locationElement.textContent.trim();
89        }
90        
91        // Get age
92        let age = null;
93        const ageMatch = html.match(/(\d+)\s*years?\s*old/i);
94        if (ageMatch) {
95            age = parseInt(ageMatch[1]);
96        }
97        
98        // Get profile URL
99        const profileUrl = `https://fetlife.com/users/${userId}`;
100        
101        // Get avatar
102        let avatarUrl = null;
103        const avatarImg = doc.querySelector('img[alt*="avatar"]');
104        if (avatarImg) {
105            avatarUrl = avatarImg.src;
106        }
107        
108        return {
109            userId,
110            nickname,
111            gender,
112            location,
113            age,
114            profileUrl,
115            avatarUrl
116        };
117    }
118
119    // Search for users
120    async function searchUsers(onProgress, onComplete) {
121        const results = [];
122        let checkedCount = 0;
123        let currentUserId = await GM.getValue('lastSearchedUserId', CONFIG.startUserId);
124        
125        console.log(`Starting search from user ID: ${currentUserId}`);
126        
127        for (let i = 0; i < CONFIG.maxProfiles; i++) {
128            try {
129                onProgress(`Checking profile ${checkedCount + 1}/${CONFIG.maxProfiles}...`, results.length);
130                
131                const html = await fetchUserProfile(currentUserId);
132                
133                if (html) {
134                    const profile = parseUserProfile(html, currentUserId);
135                    
136                    console.log(`Checked user ${currentUserId}:`, profile);
137                    
138                    // Check if matches criteria
139                    if (profile.gender === 'Female' && isNewEnglandLocation(profile.location)) {
140                        results.push(profile);
141                        console.log(`Found match: ${profile.nickname} from ${profile.location}`);
142                    }
143                }
144                
145                checkedCount++;
146                currentUserId++;
147                
148                // Save progress
149                await GM.setValue('lastSearchedUserId', currentUserId);
150                
151                // Delay to avoid rate limiting
152                await new Promise(resolve => setTimeout(resolve, CONFIG.searchDelay));
153                
154            } catch (error) {
155                console.error(`Error checking user ${currentUserId}:`, error);
156                currentUserId++;
157            }
158        }
159        
160        onComplete(results);
161    }
162
163    // Create search UI
164    function createSearchUI() {
165        // Check if UI already exists
166        if (document.getElementById('fetlife-search-panel')) {
167            return;
168        }
169        
170        // Create panel
171        const panel = document.createElement('div');
172        panel.id = 'fetlife-search-panel';
173        panel.style.cssText = `
174            position: fixed;
175            top: 70px;
176            right: 20px;
177            width: 400px;
178            max-height: 80vh;
179            background: #1a1a1a;
180            border: 1px solid #333;
181            border-radius: 8px;
182            padding: 20px;
183            z-index: 10000;
184            color: #e0e0e0;
185            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
186            box-shadow: 0 4px 20px rgba(0,0,0,0.5);
187            overflow-y: auto;
188        `;
189        
190        panel.innerHTML = `
191            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
192                <h3 style="margin: 0; color: #fff; font-size: 18px;">New England Female Search</h3>
193                <button id="close-search-panel" style="background: transparent; border: none; color: #999; font-size: 24px; cursor: pointer; padding: 0; line-height: 1;">&times;</button>
194            </div>
195            
196            <div style="margin-bottom: 15px;">
197                <p style="margin: 0 0 10px 0; font-size: 13px; color: #999;">
198                    Search for female users in New England states (CT, ME, MA, NH, RI, VT)
199                </p>
200            </div>
201            
202            <div style="margin-bottom: 15px;">
203                <button id="start-search-btn" style="
204                    width: 100%;
205                    padding: 12px;
206                    background: #d50000;
207                    color: white;
208                    border: none;
209                    border-radius: 4px;
210                    font-size: 14px;
211                    font-weight: 600;
212                    cursor: pointer;
213                    transition: background 0.2s;
214                ">Start Search</button>
215                
216                <button id="stop-search-btn" style="
217                    width: 100%;
218                    padding: 12px;
219                    background: #666;
220                    color: white;
221                    border: none;
222                    border-radius: 4px;
223                    font-size: 14px;
224                    font-weight: 600;
225                    cursor: pointer;
226                    margin-top: 8px;
227                    display: none;
228                ">Stop Search</button>
229                
230                <button id="reset-search-btn" style="
231                    width: 100%;
232                    padding: 8px;
233                    background: transparent;
234                    color: #999;
235                    border: 1px solid #444;
236                    border-radius: 4px;
237                    font-size: 12px;
238                    cursor: pointer;
239                    margin-top: 8px;
240                ">Reset Search Position</button>
241            </div>
242            
243            <div id="search-status" style="
244                padding: 10px;
245                background: #252525;
246                border-radius: 4px;
247                margin-bottom: 15px;
248                font-size: 13px;
249                display: none;
250            ">
251                <div id="status-text">Ready to search...</div>
252                <div id="status-found" style="color: #4caf50; margin-top: 5px;">Found: 0 users</div>
253            </div>
254            
255            <div id="search-results" style="margin-top: 15px;"></div>
256        `;
257        
258        document.body.appendChild(panel);
259        
260        // Add hover effect to start button
261        const startBtn = document.getElementById('start-search-btn');
262        startBtn.addEventListener('mouseenter', () => {
263            startBtn.style.background = '#b00000';
264        });
265        startBtn.addEventListener('mouseleave', () => {
266            startBtn.style.background = '#d50000';
267        });
268        
269        // Event listeners
270        let searchAborted = false;
271        
272        document.getElementById('close-search-panel').addEventListener('click', () => {
273            panel.remove();
274        });
275        
276        document.getElementById('start-search-btn').addEventListener('click', async () => {
277            searchAborted = false;
278            document.getElementById('start-search-btn').style.display = 'none';
279            document.getElementById('stop-search-btn').style.display = 'block';
280            document.getElementById('search-status').style.display = 'block';
281            document.getElementById('search-results').innerHTML = '';
282            
283            await searchUsers(
284                (statusText, foundCount) => {
285                    if (searchAborted) return;
286                    document.getElementById('status-text').textContent = statusText;
287                    document.getElementById('status-found').textContent = `Found: ${foundCount} users`;
288                },
289                (results) => {
290                    document.getElementById('start-search-btn').style.display = 'block';
291                    document.getElementById('stop-search-btn').style.display = 'none';
292                    document.getElementById('status-text').textContent = 'Search complete!';
293                    displayResults(results);
294                }
295            );
296        });
297        
298        document.getElementById('stop-search-btn').addEventListener('click', () => {
299            searchAborted = true;
300            document.getElementById('start-search-btn').style.display = 'block';
301            document.getElementById('stop-search-btn').style.display = 'none';
302            document.getElementById('status-text').textContent = 'Search stopped.';
303        });
304        
305        document.getElementById('reset-search-btn').addEventListener('click', async () => {
306            await GM.setValue('lastSearchedUserId', CONFIG.startUserId);
307            alert('Search position reset to beginning.');
308        });
309    }
310
311    // Display search results
312    function displayResults(results) {
313        const resultsContainer = document.getElementById('search-results');
314        
315        if (results.length === 0) {
316            resultsContainer.innerHTML = '<p style="color: #999; text-align: center;">No users found matching criteria.</p>';
317            return;
318        }
319        
320        resultsContainer.innerHTML = '<h4 style="margin: 0 0 10px 0; color: #fff; font-size: 16px;">Results:</h4>';
321        
322        results.forEach(user => {
323            const userCard = document.createElement('div');
324            userCard.style.cssText = `
325                background: #252525;
326                border-radius: 4px;
327                padding: 12px;
328                margin-bottom: 10px;
329                display: flex;
330                gap: 12px;
331                align-items: center;
332                transition: background 0.2s;
333            `;
334            
335            userCard.addEventListener('mouseenter', () => {
336                userCard.style.background = '#2a2a2a';
337            });
338            userCard.addEventListener('mouseleave', () => {
339                userCard.style.background = '#252525';
340            });
341            
342            userCard.innerHTML = `
343                ${user.avatarUrl ? `<img src="${user.avatarUrl}" style="width: 50px; height: 50px; border-radius: 4px; object-fit: cover;">` : ''}
344                <div style="flex: 1;">
345                    <div style="font-weight: 600; color: #fff; margin-bottom: 4px;">
346                        <a href="${user.profileUrl}" target="_blank" style="color: #d50000; text-decoration: none;">${user.nickname}</a>
347                    </div>
348                    <div style="font-size: 12px; color: #999;">
349                        ${user.age ? `${user.age} years old • ` : ''}${user.location || 'Location unknown'}
350                    </div>
351                </div>
352            `;
353            
354            resultsContainer.appendChild(userCard);
355        });
356    }
357
358    // Add search button to navigation
359    function addSearchButton() {
360        const nav = document.querySelector('#main-nav');
361        if (!nav || document.getElementById('fetlife-search-btn')) {
362            return;
363        }
364        
365        const buttonContainer = document.createElement('div');
366        buttonContainer.style.cssText = `
367            position: fixed;
368            bottom: 20px;
369            right: 20px;
370            z-index: 9999;
371        `;
372        
373        const button = document.createElement('button');
374        button.id = 'fetlife-search-btn';
375        button.textContent = '🔍 Search New England';
376        button.style.cssText = `
377            padding: 12px 20px;
378            background: #d50000;
379            color: white;
380            border: none;
381            border-radius: 25px;
382            font-size: 14px;
383            font-weight: 600;
384            cursor: pointer;
385            box-shadow: 0 4px 12px rgba(213, 0, 0, 0.4);
386            transition: all 0.2s;
387        `;
388        
389        button.addEventListener('mouseenter', () => {
390            button.style.background = '#b00000';
391            button.style.transform = 'translateY(-2px)';
392            button.style.boxShadow = '0 6px 16px rgba(213, 0, 0, 0.5)';
393        });
394        
395        button.addEventListener('mouseleave', () => {
396            button.style.background = '#d50000';
397            button.style.transform = 'translateY(0)';
398            button.style.boxShadow = '0 4px 12px rgba(213, 0, 0, 0.4)';
399        });
400        
401        button.addEventListener('click', () => {
402            createSearchUI();
403        });
404        
405        buttonContainer.appendChild(button);
406        document.body.appendChild(buttonContainer);
407    }
408
409    // Initialize
410    function init() {
411        console.log('FetLife New England Female User Search extension loaded');
412        
413        // Wait for page to be ready
414        if (document.readyState === 'loading') {
415            document.addEventListener('DOMContentLoaded', addSearchButton);
416        } else {
417            addSearchButton();
418        }
419        
420        // Re-add button on navigation changes (for SPA behavior)
421        const observer = new MutationObserver(debounce(() => {
422            if (!document.getElementById('fetlife-search-btn')) {
423                addSearchButton();
424            }
425        }, 1000));
426        
427        observer.observe(document.body, {
428            childList: true,
429            subtree: true
430        });
431    }
432
433    init();
434})();