• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

URL EXTRACTOR - INTERACTIVE APP SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>URL Extractor</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        :root {
            --bg-primary: #0f172a;
            --bg-secondary: #1e293b;
            --bg-tertiary: #334155;
            --border-color: #475569;
            --text-primary: #f1f5f9;
            --text-secondary: #cbd5e1;
            --text-muted: #94a3b8;
            --accent-blue: #3b82f6;
            --accent-green: #10b981;
            --accent-yellow: #f59e0b;
            --accent-red: #ef4444;
        }

        :root.light-mode {
            --bg-primary: #f8fafc;
            --bg-secondary: #ffffff;
            --bg-tertiary: #f1f5f9;
            --border-color: #e2e8f0;
            --text-primary: #0f172a;
            --text-secondary: #334155;
            --text-muted: #64748b;
            --accent-blue: #3b82f6;
            --accent-green: #10b981;
            --accent-yellow: #f59e0b;
            --accent-red: #ef4444;
        }

        html {
            height: auto;
            min-height: 100vh;
            overflow-y: auto;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            background: var(--bg-primary);
            color: var(--text-secondary);
            min-height: 100vh;
            height: auto;
            overflow-y: auto;
            padding: 0;
            margin: 0;
            transition: background-color 0.3s ease, color 0.3s ease;
        }

        .app-wrapper {
            min-height: 100vh;
            height: auto;
            padding: 20px;
            background: var(--bg-primary);
            transition: background-color 0.3s ease;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            background: var(--bg-secondary);
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
            height: auto;
            min-height: 600px;
            transition: background-color 0.3s ease, box-shadow 0.3s ease;
        }

        .light-mode .container {
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
        }

        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20px 30px;
            background: var(--bg-tertiary);
            border-bottom: 1px solid var(--border-color);
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .logo {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .logo-icon {
            font-size: 28px;
            color: #60a5fa;
        }

        .logo-text {
            font-size: 22px;
            font-weight: 600;
            color: var(--text-primary);
        }

        .theme-toggle {
            background: var(--border-color);
            border: none;
            color: var(--text-secondary);
            padding: 10px 16px;
            border-radius: 8px;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 14px;
            transition: all 0.2s ease;
        }

        .theme-toggle:hover {
            background: var(--text-muted);
        }

        .main-content {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 0;
            height: auto;
            min-height: 500px;
        }

        @media (max-width: 1024px) {
            .main-content {
                grid-template-columns: 1fr;
                min-height: 800px;
            }
        }

        .left-panel {
            background: var(--bg-secondary);
            border-right: 1px solid var(--border-color);
            padding: 30px;
            display: flex;
            flex-direction: column;
            height: auto;
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .panel-title {
            font-size: 18px;
            font-weight: 600;
            color: var(--text-primary);
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 20px;
        }

        .panel-icon {
            color: #60a5fa;
        }

        .url-input-wrapper {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        #url-input {
            flex: 1;
            padding: 14px 18px;
            background: var(--bg-primary);
            border: 1px solid var(--border-color);
            border-radius: 8px;
            color: var(--text-primary);
            font-size: 15px;
            outline: none;
            transition: all 0.3s ease;
        }

        #url-input:focus {
            border-color: var(--accent-blue);
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
        }

        #fetch-button {
            padding: 14px 28px;
            background: var(--accent-blue);
            color: white;
            border: none;
            border-radius: 8px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
            white-space: nowrap;
            min-width: 160px;
        }

        #fetch-button:hover:not(:disabled) {
            background: #2563eb;
            transform: translateY(-1px);
        }

        #fetch-button:disabled {
            background: #4b5563;
            cursor: not-allowed;
        }

        #fetch-button.loading {
            background: var(--accent-yellow);
            position: relative;
            padding-left: 50px;
        }

        #fetch-button.loading:before {
            content: '';
            position: absolute;
            left: 20px;
            top: 50%;
            transform: translateY(-50%);
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            to { transform: translateY(-50%) rotate(360deg); }
        }

        .proxy-notice {
            background: rgba(245, 158, 11, 0.1);
            border: 1px solid rgba(245, 158, 11, 0.3);
            border-radius: 8px;
            padding: 12px 16px;
            font-size: 14px;
            color: #fbbf24;
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 20px;
        }

        .light-mode .proxy-notice {
            background: rgba(245, 158, 11, 0.05);
            color: #92400e;
        }

        .proxy-notice a {
            color: #60a5fa;
            text-decoration: none;
            font-weight: 500;
        }

        .proxy-notice a:hover {
            text-decoration: underline;
        }

        .warning-icon {
            color: var(--accent-yellow);
            font-size: 18px;
        }

        .stats-container {
            background: var(--bg-primary);
            border-radius: 8px;
            padding: 20px;
            margin-top: 20px;
            display: none;
            transition: background-color 0.3s ease;
        }

        .stats-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
            margin-bottom: 20px;
        }

        @media (min-width: 768px) {
            .stats-grid {
                grid-template-columns: repeat(4, 1fr);
            }
        }

        .stat-card {
            background: var(--bg-secondary);
            border-radius: 8px;
            padding: 16px;
            border: 1px solid var(--border-color);
            text-align: center;
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .stat-value {
            font-size: 24px;
            font-weight: 700;
            color: #60a5fa;
            margin-bottom: 6px;
        }

        .stat-label {
            font-size: 12px;
            color: var(--text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .url-types {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .url-type-tag {
            background: var(--border-color);
            color: var(--text-secondary);
            padding: 6px 12px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 500;
            transition: background-color 0.3s ease, color 0.3s ease;
        }

        .right-panel {
            background: var(--bg-secondary);
            display: flex;
            flex-direction: column;
            height: auto;
            min-height: 500px;
            transition: background-color 0.3s ease;
        }

        .results-header {
            padding: 25px 30px;
            background: var(--bg-tertiary);
            border-bottom: 1px solid var(--border-color);
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .results-header .panel-title {
            margin-bottom: 0;
        }

        .filter-controls {
            padding: 20px 30px;
            background: var(--bg-secondary);
            border-bottom: 1px solid var(--border-color);
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            align-items: center;
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .filter-group {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .filter-label {
            font-size: 14px;
            color: var(--text-muted);
            white-space: nowrap;
        }

        .filter-input {
            padding: 10px 14px;
            background: var(--bg-primary);
            border: 1px solid var(--border-color);
            border-radius: 6px;
            color: var(--text-primary);
            font-size: 14px;
            min-width: 200px;
            width: 100%;
            transition: all 0.3s ease;
        }

        .filter-select {
            padding: 10px 14px;
            background: var(--bg-primary);
            border: 1px solid var(--border-color);
            border-radius: 6px;
            color: var(--text-primary);
            font-size: 14px;
            min-width: 150px;
            transition: all 0.3s ease;
        }

        .checkbox-group {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .checkbox-label {
            font-size: 14px;
            color: var(--text-secondary);
        }

        input[type="checkbox"] {
            width: 18px;
            height: 18px;
            accent-color: var(--accent-blue);
        }

        .results-content {
            flex: 1;
            padding: 20px 30px;
            background: var(--bg-primary);
            overflow-y: auto;
            max-height: 400px;
            min-height: 300px;
            transition: background-color 0.3s ease;
        }

        @media (min-height: 800px) {
            .results-content {
                max-height: 500px;
            }
        }

        #results {
            font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
            font-size: 13px;
            line-height: 1.6;
            color: var(--text-secondary);
            white-space: pre-wrap;
            word-break: break-all;
        }

        .url-item {
            padding: 12px 16px;
            margin-bottom: 10px;
            background: var(--bg-secondary);
            border: 1px solid var(--border-color);
            border-radius: 8px;
            display: flex;
            align-items: center;
            gap: 12px;
            transition: all 0.2s ease;
        }

        .url-item:hover {
            border-color: var(--accent-blue);
            background: rgba(59, 130, 246, 0.05);
        }

        .light-mode .url-item:hover {
            background: rgba(59, 130, 246, 0.1);
        }

        .url-badge {
            font-size: 11px;
            padding: 4px 10px;
            border-radius: 6px;
            font-weight: 600;
            min-width: 45px;
            text-align: center;
            flex-shrink: 0;
        }

        .badge-http { background: #1e40af; color: #dbeafe; }
        .badge-https { background: #065f46; color: #d1fae5; }
        .badge-image { background: #7c2d12; color: #fed7aa; }
        .badge-css { background: #5b21b6; color: #ede9fe; }
        .badge-js { background: #92400e; color: #fef3c7; }
        .badge-internal { background: #374151; color: #d1d5db; }
        .badge-external { background: #78350f; color: #fde68a; }

        .light-mode .badge-internal { background: #d1d5db; color: #374151; }
        .light-mode .badge-external { background: #fde68a; color: #78350f; }

        .url-text {
            flex: 1;
            word-break: break-all;
            color: var(--text-primary);
            font-family: inherit;
        }

        .action-buttons {
            padding: 20px 30px;
            background: var(--bg-tertiary);
            border-top: 1px solid var(--border-color);
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .action-button {
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            transition: all 0.2s ease;
            flex: 1;
            min-width: 120px;
        }

        @media (max-width: 768px) {
            .action-buttons {
                flex-direction: column;
            }

            .action-button {
                min-width: 100%;
            }
        }

        .action-button.primary {
            background: var(--accent-blue);
            color: white;
        }

        .action-button.primary:hover {
            background: #2563eb;
        }

        .action-button.secondary {
            background: var(--border-color);
            color: var(--text-primary);
        }

        .action-button.secondary:hover {
            background: var(--text-muted);
        }

        .action-button.warning {
            background: var(--accent-yellow);
            color: white;
        }

        .action-button.warning:hover {
            background: #d97706;
        }

        .action-button.success {
            background: var(--accent-green);
            color: white;
        }

        .action-button.success:hover {
            background: #059669;
        }

        .action-button.danger {
            background: var(--accent-red);
            color: white;
        }

        .action-button.danger:hover {
            background: #dc2626;
        }

        .status-bar {
            padding: 15px 30px;
            background: var(--bg-tertiary);
            border-top: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 14px;
            color: var(--text-muted);
            transition: background-color 0.3s ease, border-color 0.3s ease;
        }

        .error-message {
            background: rgba(239, 68, 68, 0.1);
            border: 1px solid rgba(239, 68, 68, 0.3);
            color: #fca5a5;
            padding: 12px 16px;
            border-radius: 8px;
            margin-top: 15px;
            font-size: 14px;
            display: none;
        }

        .light-mode .error-message {
            color: #dc2626;
            background: rgba(239, 68, 68, 0.05);
        }

        .error-message.visible {
            display: block;
        }

        .success-message {
            background: rgba(16, 185, 129, 0.1);
            border: 1px solid rgba(16, 185, 129, 0.3);
            color: #a7f3d0;
            padding: 12px 16px;
            border-radius: 8px;
            margin-top: 15px;
            font-size: 14px;
            display: none;
        }

        .light-mode .success-message {
            color: #059669;
            background: rgba(16, 185, 129, 0.05);
        }

        .success-message.visible {
            display: block;
        }

        ::-webkit-scrollbar {
            width: 10px;
            height: 10px;
        }

        ::-webkit-scrollbar-track {
            background: var(--bg-secondary);
            border-radius: 5px;
        }

        ::-webkit-scrollbar-thumb {
            background: var(--border-color);
            border-radius: 5px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: var(--text-muted);
        }

        @media (max-width: 768px) {
            .app-wrapper {
                padding: 10px;
            }

            .container {
                border-radius: 8px;
            }

            .header {
                padding: 15px 20px;
                flex-direction: column;
                gap: 15px;
                align-items: flex-start;
            }

            .left-panel, .right-panel {
                padding: 20px;
            }

            .url-input-wrapper {
                flex-direction: column;
            }

            #fetch-button {
                width: 100%;
            }

            .filter-controls {
                flex-direction: column;
                align-items: stretch;
                gap: 12px;
            }

            .filter-group {
                flex-direction: column;
                align-items: stretch;
            }

            .filter-input, .filter-select {
                min-width: 100%;
            }

            .results-header, .status-bar {
                padding: 15px 20px;
            }
        }

        body.in-iframe {
            overflow-y: visible;
            height: auto;
        }

        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(15, 23, 42, 0.8);
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 1000;
            transition: background-color 0.3s ease;
        }

        .light-mode .loading-overlay {
            background: rgba(248, 250, 252, 0.8);
        }

        .loading-overlay.visible {
            display: flex;
        }

        .spinner {
            width: 50px;
            height: 50px;
            border: 5px solid var(--border-color);
            border-top-color: var(--accent-blue);
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
    </style>
</head>
<body>
    <div class="loading-overlay" id="loading-overlay">
        <div class="spinner"></div>
    </div>

    <div class="app-wrapper">
        <div class="container">
            <div class="header">
                <div class="logo">
                    <span class="logo-icon">🌐</span>
                    <span class="logo-text">URL Extractor</span>
                </div>
                <button class="theme-toggle" id="theme-toggle">
                    <span id="theme-icon">☀️</span>
                    <span id="theme-text">Light Mode</span>
                </button>
            </div>

            <div class="main-content">
                <div class="left-panel">
                    <div class="panel-title">
                        <span class="panel-icon">🔗</span>
                        <span>Webpage URL Extractor</span>
                    </div>

                    <div class="url-input-wrapper">
                        <input
                            type="url"
                            id="url-input"
                            placeholder="https://example.com"
                            autocomplete="off"
                        >
                        <button id="fetch-button">Fetch & Extract URLs</button>
                    </div>

                    <div class="proxy-notice">
                        <span class="warning-icon">⚠</span>
                        <span>
                            Using CORS proxy for cross-origin requests.
                            <a href="#" id="proxy-info">Learn more</a>
                        </span>
                    </div>

                    <div id="error-message" class="error-message"></div>
                    <div id="success-message" class="success-message"></div>

                    <div id="stats-container" class="stats-container">
                        <div class="stats-grid">
                            <div class="stat-card">
                                <div class="stat-value" id="total-count">0</div>
                                <div class="stat-label">Total URLs</div>
                            </div>
                            <div class="stat-card">
                                <div class="stat-value" id="unique-count">0</div>
                                <div class="stat-label">Unique URLs</div>
                            </div>
                            <div class="stat-card">
                                <div class="stat-value" id="internal-count">0</div>
                                <div class="stat-label">Internal Links</div>
                            </div>
                            <div class="stat-card">
                                <div class="stat-value" id="external-count">0</div>
                                <div class="stat-label">External Links</div>
                            </div>
                        </div>

                        <div class="url-types" id="url-types"></div>
                    </div>
                </div>

                <div class="right-panel">
                    <div class="results-header">
                        <div class="panel-title">
                            <span class="panel-icon">📋</span>
                            <span>Extracted URLs</span>
                        </div>
                    </div>

                    <div class="filter-controls">
                        <div class="filter-group">
                            <span class="filter-label">Filter:</span>
                            <input
                                type="text"
                                id="filter-input"
                                class="filter-input"
                                placeholder="Filter by domain or keyword"
                            >
                        </div>

                        <div class="filter-group">
                            <span class="filter-label">Type:</span>
                            <select id="type-filter" class="filter-select">
                                <option value="all">All Types</option>
                                <option value="internal">Internal Links</option>
                                <option value="external">External Links</option>
                                <option value="image">Images</option>
                                <option value="css">CSS Files</option>
                                <option value="js">JavaScript Files</option>
                            </select>
                        </div>

                        <div class="checkbox-group">
                            <input type="checkbox" id="unique-only" checked>
                            <label for="unique-only" class="checkbox-label">Unique only</label>
                        </div>

                        <div class="checkbox-group">
                            <input type="checkbox" id="show-badges" checked>
                            <label for="show-badges" class="checkbox-label">Show badges</label>
                        </div>
                    </div>

                    <div class="results-content">
                        <div id="results"></div>
                    </div>

                    <div class="action-buttons">
                        <button id="sort-button" class="action-button secondary">Sort A-Z</button>
                        <button id="remove-dupes-button" class="action-button warning">Remove Duplicates</button>
                        <button id="copy-button" class="action-button primary">Copy All</button>
                        <button id="clear-button" class="action-button danger">Clear All</button>
                    </div>
                </div>
            </div>

            <div class="status-bar">
                <span id="status">Ready. Enter a URL to begin.</span>
                <span id="url-count">0 URLs</span>
            </div>
        </div>
    </div>

    <script>
        const urlInput = document.getElementById('url-input');
        const fetchButton = document.getElementById('fetch-button');
        const results = document.getElementById('results');
        const copyButton = document.getElementById('copy-button');
        const clearButton = document.getElementById('clear-button');
        const removeDupesButton = document.getElementById('remove-dupes-button');
        const sortButton = document.getElementById('sort-button');
        const errorMessage = document.getElementById('error-message');
        const successMessage = document.getElementById('success-message');
        const statsContainer = document.getElementById('stats-container');
        const filterInput = document.getElementById('filter-input');
        const typeFilter = document.getElementById('type-filter');
        const uniqueOnlyCheckbox = document.getElementById('unique-only');
        const showBadgesCheckbox = document.getElementById('show-badges');
        const status = document.getElementById('status');
        const urlCount = document.getElementById('url-count');
        const proxyInfo = document.getElementById('proxy-info');
        const themeToggle = document.getElementById('theme-toggle');
        const themeIcon = document.getElementById('theme-icon');
        const themeText = document.getElementById('theme-text');
        const loadingOverlay = document.getElementById('loading-overlay');

        const totalCountElement = document.getElementById('total-count');
        const uniqueCountElement = document.getElementById('unique-count');
        const internalCountElement = document.getElementById('internal-count');
        const externalCountElement = document.getElementById('external-count');
        const urlTypesElement = document.getElementById('url-types');

        let extractedUrls = [];
        let currentFilter = '';
        let currentTypeFilter = 'all';
        let showUniqueOnly = true;
        let showBadges = true;

        const CORS_PROXIES = [
            'https://api.allorigins.win/raw?url=',
            'https://corsproxy.io/?',
            'https://api.codetabs.com/v1/proxy?quest='
        ];

        const isInIframe = window.self !== window.top;

        if (isInIframe) {
            console.log('Running in iframe - applying optimizations');
            document.body.classList.add('in-iframe');

            const sendHeightToParent = () => {
                const height = document.documentElement.scrollHeight;
                window.parent.postMessage({
                    type: 'iframeHeight',
                    height: height
                }, '*');
            };

            setTimeout(sendHeightToParent, 100);

            const observer = new MutationObserver(sendHeightToParent);
            observer.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true
            });

            window.addEventListener('resize', sendHeightToParent);
        }

        function init() {
            const savedUrl = localStorage.getItem('lastFetchedUrl');
            if (savedUrl) {
                urlInput.value = savedUrl;
            }

            const savedTheme = localStorage.getItem('theme');
            if (savedTheme === 'light') {
                document.documentElement.classList.add('light-mode');
                themeIcon.textContent = '🌙';
                themeText.textContent = 'Dark Mode';
            } else {
                themeIcon.textContent = '☀️';
                themeText.textContent = 'Light Mode';
            }

            urlInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    fetchAndExtract();
                }
            });

            fetchButton.addEventListener('click', fetchAndExtract);
            copyButton.addEventListener('click', copyResults);
            clearButton.addEventListener('click', clearAll);
            removeDupesButton.addEventListener('click', removeDuplicates);
            sortButton.addEventListener('click', sortResults);
            filterInput.addEventListener('input', updateFilter);
            typeFilter.addEventListener('change', updateTypeFilter);
            uniqueOnlyCheckbox.addEventListener('change', updateUniqueFilter);
            showBadgesCheckbox.addEventListener('change', updateBadgesDisplay);
            proxyInfo.addEventListener('click', showProxyInfo);
            themeToggle.addEventListener('click', toggleTheme);

            urlInput.focus();
        }

        async function fetchAndExtract() {
            const url = urlInput.value.trim();

            if (!url) {
                showError('Please enter a valid URL');
                urlInput.focus();
                return;
            }

            try {
                new URL(url.startsWith('http') ? url : \`https://\${url}\`);
            } catch {
                showError('Please enter a valid URL (e.g., https://example.com)');
                urlInput.focus();
                return;
            }

            // Add https:// if not present
            const fullUrl = url.startsWith('http') ? url : \`https://\${url}\`;

            localStorage.setItem('lastFetchedUrl', fullUrl);

            fetchButton.disabled = true;
            fetchButton.classList.add('loading');
            fetchButton.textContent = 'Fetching...';
            loadingOverlay.classList.add('visible');

            clearResults();
            hideMessages();
            showStatus(\`Fetching \${new URL(fullUrl).hostname}...\`);

            try {
                let html = null;
                let lastError = null;

                for (let i = 0; i < CORS_PROXIES.length; i++) {
                    const proxyUrl = CORS_PROXIES[i] + encodeURIComponent(fullUrl);

                    try {
                        showStatus(\`Trying proxy \${i + 1}/\${CORS_PROXIES.length}...\`);
                        const response = await fetchWithTimeout(proxyUrl, 15000);

                        if (!response.ok) {
                            throw new Error(\`HTTP \${response.status}\`);
                        }

                        html = await response.text();
                        break;
                    } catch (error) {
                        lastError = error;
                        console.warn(\`Proxy \${i} failed:\`, error);
                    }
                }

                if (!html) {
                    throw new Error(\`All proxies failed. Last error: \${lastError?.message || 'Unknown error'}\`);
                }

                showStatus('Extracting URLs from HTML...');

                const urls = extractUrlsFromHTML(html, fullUrl);

                if (urls.length === 0) {
                    throw new Error('No URLs found on this webpage');
                }

                extractedUrls = urls;
                displayResults();
                updateStats();

                showSuccess(\`Successfully extracted \${urls.length} URLs from \${new URL(fullUrl).hostname}\`);
                statsContainer.style.display = 'block';

            } catch (error) {
                console.error('Fetch error:', error);
                showError(\`Failed to fetch webpage: \${error.message}. Try a different URL or check your connection.\`);
            } finally {
                fetchButton.disabled = false;
                fetchButton.classList.remove('loading');
                fetchButton.textContent = 'Fetch & Extract URLs';
                loadingOverlay.classList.remove('visible');
            }
        }

        function extractUrlsFromHTML(html, baseUrl) {
            const urls = [];
            const base = new URL(baseUrl);

            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            const urlAttributes = [
                { selector: 'a[href]', attr: 'href' },
                { selector: 'img[src]', attr: 'src' },
                { selector: 'img[srcset]', attr: 'srcset' },
                { selector: 'link[href]', attr: 'href' },
                { selector: 'script[src]', attr: 'src' },
                { selector: 'iframe[src]', attr: 'src' },
                { selector: 'source[src]', attr: 'src' },
                { selector: 'video[src]', attr: 'src' },
                { selector: 'audio[src]', attr: 'src' },
                { selector: 'embed[src]', attr: 'src' },
                { selector: 'object[data]', attr: 'data' },
                { selector: 'form[action]', attr: 'action' },
                { selector: 'meta[content]', extractor: (el) => {
                    const property = el.getAttribute('property');
                    const itemprop = el.getAttribute('itemprop');
                    if (property && ['og:image', 'og:url', 'og:video'].includes(property)) {
                        return el.getAttribute('content');
                    }
                    if (itemprop && ['image', 'url'].includes(itemprop)) {
                        return el.getAttribute('content');
                    }
                    return null;
                }},
                { selector: '*[style*="url("]', extractor: extractFromStyle },
                { selector: '*[data-src]', attr: 'data-src' },
                { selector: '*[data-href]', attr: 'data-href' },
                { selector: '*[data-url]', attr: 'data-url' }
            ];

            urlAttributes.forEach(({ selector, attr, extractor }) => {
                const elements = doc.querySelectorAll(selector);

                elements.forEach(element => {
                    let url = null;

                    if (extractor) {
                        url = extractor(element);
                    } else if (attr === 'srcset') {
                        const srcset = element.getAttribute('srcset');
                        if (srcset) {
                            srcset.split(',').forEach(part => {
                                const src = part.trim().split(' ')[0];
                                if (src) url = src;
                            });
                        }
                    } else {
                        url = element.getAttribute(attr);
                    }

                    if (url && url.trim() && !url.startsWith('javascript:') && !url.startsWith('mailto:')) {
                        const cleanUrl = cleanAndNormalizeUrl(url.trim(), base);
                        if (cleanUrl) {
                            urls.push({
                                url: cleanUrl,
                                type: getUrlType(cleanUrl, base),
                                original: url.trim()
                            });
                        }
                    }
                });
            });

            const scripts = doc.querySelectorAll('script:not([src]), style');
            scripts.forEach(element => {
                const text = element.textContent;
                const urlRegex = /(https?:\\/\\/[^\\s<>"\\'{}|\\\\^\`\\[\\]]+)|(src\\s*=\\s*["']([^"']+)["'])|(href\\s*=\\s*["']([^"']+)["'])|(url\\(["']?([^"')]+)["']?\\))/gi;
                let match;

                while ((match = urlRegex.exec(text)) !== null) {
                    const url = match[1] || match[3] || match[5] || match[7];
                    if (url && url.trim()) {
                        const cleanUrl = cleanAndNormalizeUrl(url.trim(), base);
                        if (cleanUrl) {
                            urls.push({
                                url: cleanUrl,
                                type: getUrlType(cleanUrl, base),
                                original: url.trim()
                            });
                        }
                    }
                }
            });

            return urls;
        }

        function extractFromStyle(element) {
            const style = element.getAttribute('style');
            if (!style) return null;

            const urlMatch = style.match(/url\\(['"]?([^'")]+)['"]?\\)/i);
            return urlMatch ? urlMatch[1] : null;
        }

        function cleanAndNormalizeUrl(url, base) {
            if (url.startsWith('data:') || url.startsWith('#') || url === '') {
                return null;
            }

            url = url.split('#')[0];

            try {
                if (url.startsWith('//')) {
                    url = base.protocol + url;
                } else if (url.startsWith('/')) {
                    url = base.origin + url;
                } else if (!url.startsWith('http')) {
                    url = new URL(url, base.origin).href;
                }

                const urlObj = new URL(url);
                const trackingParams = ['utm_', 'fbclid', 'gclid', 'msclkid', 'dclid', 'ref', 'source'];
                urlObj.searchParams.forEach((value, key) => {
                    if (trackingParams.some(param => key.startsWith(param))) {
                        urlObj.searchParams.delete(key);
                    }
                });

                let cleanUrl = urlObj.href;
                if (cleanUrl.endsWith('/')) {
                    cleanUrl = cleanUrl.slice(0, -1);
                }

                return cleanUrl;
            } catch {
                return url;
            }
        }

        function getUrlType(url, base) {
            try {
                const urlObj = new URL(url);
                const baseObj = new URL(base.origin);

                const isInternal = urlObj.hostname === baseObj.hostname ||
                                  urlObj.hostname.endsWith('.' + baseObj.hostname) ||
                                  !urlObj.hostname.includes('.');

                const pathname = urlObj.pathname.toLowerCase();
                if (pathname.match(/\\.(jpg|jpeg|png|gif|webp|svg|ico|bmp|tiff)$/)) {
                    return isInternal ? 'internal-image' : 'external-image';
                }
                if (pathname.match(/\\.css$/)) {
                    return isInternal ? 'internal-css' : 'external-css';
                }
                if (pathname.match(/\\.js$/)) {
                    return isInternal ? 'internal-js' : 'external-js';
                }
                if (pathname.match(/\\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$/)) {
                    return isInternal ? 'internal-doc' : 'external-doc';
                }

                return isInternal ? 'internal' : 'external';
            } catch {
                return 'external';
            }
        }

        function displayResults() {
            let filteredUrls = [...extractedUrls];

            if (currentFilter) {
                const filterLower = currentFilter.toLowerCase();
                filteredUrls = filteredUrls.filter(item =>
                    item.url.toLowerCase().includes(filterLower) ||
                    item.original.toLowerCase().includes(filterLower)
                );
            }

            if (currentTypeFilter !== 'all') {
                filteredUrls = filteredUrls.filter(item =>
                    item.type.includes(currentTypeFilter)
                );
            }

            if (showUniqueOnly) {
                const seen = new Set();
                filteredUrls = filteredUrls.filter(item => {
                    if (seen.has(item.url)) return false;
                    seen.add(item.url);
                    return true;
                });
            }

            if (filteredUrls.length === 0) {
                results.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);">No URLs match the current filters.</div>';
                urlCount.textContent = '0 URLs';
                return;
            }

            let output = '';
            filteredUrls.forEach(item => {
                if (showBadges) {
                    const badgeClass = getBadgeClass(item.type);
                    const badgeText = getBadgeText(item.type);
                    output += \`<div class="url-item">
                        <span class="url-badge \${badgeClass}">\${badgeText}</span>
                        <span class="url-text">\${escapeHtml(item.url)}</span>
                    </div>\`;
                } else {
                    output += \`<div class="url-item">
                        <span class="url-text">\${escapeHtml(item.url)}</span>
                    </div>\`;
                }
            });

            results.innerHTML = output;
            urlCount.textContent = \`\${filteredUrls.length} URL\${filteredUrls.length !== 1 ? 's' : ''}\`;

            if (isInIframe) {
                setTimeout(() => {
                    const height = document.documentElement.scrollHeight;
                    window.parent.postMessage({
                        type: 'iframeHeight',
                        height: height
                    }, '*');
                }, 100);
            }
        }

        function getBadgeClass(type) {
            if (type.includes('image')) return 'badge-image';
            if (type.includes('css')) return 'badge-css';
            if (type.includes('js')) return 'badge-js';
            if (type.includes('doc')) return 'badge-external';
            if (type.includes('internal')) return 'badge-internal';
            return 'badge-external';
        }

        function getBadgeText(type) {
            if (type.includes('image')) return 'IMG';
            if (type.includes('css')) return 'CSS';
            if (type.includes('js')) return 'JS';
            if (type.includes('internal')) return 'INT';
            return 'EXT';
        }

        function updateStats() {
            const total = extractedUrls.length;
            const unique = new Set(extractedUrls.map(u => u.url)).size;

            const internal = extractedUrls.filter(u => u.type.includes('internal')).length;
            const external = extractedUrls.filter(u => u.type.includes('external')).length;

            totalCountElement.textContent = total;
            uniqueCountElement.textContent = unique;
            internalCountElement.textContent = internal;
            externalCountElement.textContent = external;

            const typeCounts = {};
            extractedUrls.forEach(item => {
                const mainType = item.type.includes('internal') ? 'internal' :
                               item.type.includes('external') ? 'external' : 'other';
                typeCounts[mainType] = (typeCounts[mainType] || 0) + 1;
            });

            urlTypesElement.innerHTML = '';
            Object.entries(typeCounts).forEach(([type, count]) => {
                const tag = document.createElement('span');
                tag.className = 'url-type-tag';
                tag.textContent = \`\${type}: \${count}\`;
                urlTypesElement.appendChild(tag);
            });
        }

        function updateFilter() {
            currentFilter = filterInput.value;
            displayResults();
        }

        function updateTypeFilter() {
            currentTypeFilter = typeFilter.value;
            displayResults();
        }

        function updateUniqueFilter() {
            showUniqueOnly = uniqueOnlyCheckbox.checked;
            displayResults();
        }

        function updateBadgesDisplay() {
            showBadges = showBadgesCheckbox.checked;
            displayResults();
        }

        function fetchWithTimeout(url, timeout) {
            return Promise.race([
                fetch(url),
                new Promise((_, reject) =>
                    setTimeout(() => reject(new Error('Request timeout')), timeout)
                )
            ]);
        }

        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        function showError(message) {
            errorMessage.textContent = message;
            errorMessage.classList.add('visible');
            showStatus('Error: ' + message);
        }

        function showSuccess(message) {
            successMessage.textContent = message;
            successMessage.classList.add('visible');
            showStatus(message);
        }

        function hideMessages() {
            errorMessage.classList.remove('visible');
            successMessage.classList.remove('visible');
        }

        function showStatus(message) {
            status.textContent = message;
        }

        function clearResults() {
            results.innerHTML = '';
            statsContainer.style.display = 'none';
            extractedUrls = [];
            urlCount.textContent = '0 URLs';
        }

        function clearAll() {
            urlInput.value = '';
            clearResults();
            hideMessages();
            showStatus('Ready. Enter a URL to begin.');
            localStorage.removeItem('lastFetchedUrl');
            urlInput.focus();
        }

        function copyResults() {
            if (extractedUrls.length === 0) {
                showError('No URLs to copy. Please fetch a webpage first.');
                return;
            }

            const text = extractedUrls.map(u => u.url).join('\\n');
            navigator.clipboard.writeText(text).then(() => {
                const originalText = copyButton.textContent;
                copyButton.textContent = '✓ Copied!';
                setTimeout(() => {
                    copyButton.textContent = originalText;
                }, 2000);
                showStatus(\`Copied \${extractedUrls.length} URLs to clipboard\`);
            }).catch(err => {
                console.error('Failed to copy:', err);
                showError('Failed to copy to clipboard. Please try again.');
            });
        }

        function removeDuplicates() {
            const seen = new Set();
            extractedUrls = extractedUrls.filter(item => {
                if (seen.has(item.url)) return false;
                seen.add(item.url);
                return true;
            });
            displayResults();
            updateStats();
            showStatus(\`Removed duplicates. \${extractedUrls.length} unique URLs remaining.\`);
        }

        function sortResults() {
            extractedUrls.sort((a, b) => a.url.localeCompare(b.url));
            displayResults();
            showStatus('Sorted URLs alphabetically');
        }

        function showProxyInfo(e) {
            e.preventDefault();
            alert(
                "This tool uses CORS proxies to bypass browser security restrictions.\\n\\n" +
                "CORS proxies allow fetching content from other websites by acting as intermediaries.\\n\\n" +
                "Note: Some websites may block proxy requests. If one proxy fails, the tool automatically tries others."
            );
        }

        function toggleTheme() {
            const isLightMode = document.documentElement.classList.contains('light-mode');

            if (isLightMode) {
                document.documentElement.classList.remove('light-mode');
                themeIcon.textContent = '☀️';
                themeText.textContent = 'Light Mode';
                localStorage.setItem('theme', 'dark');
            } else {
                document.documentElement.classList.add('light-mode');
                themeIcon.textContent = '🌙';
                themeText.textContent = 'Dark Mode';
                localStorage.setItem('theme', 'light');
            }
        }

        init();
    </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.