{"title":"Browse Stories | UncutFiction","description":"Browse thousands of adult fiction stories on UncutFiction. Filter by tags to find exactly what you want.","h1":"Browse Stories","content":"\n        <div class=\"browse-page\">\n            <div class=\"search-interface\">\n                <div class=\"tag-input-container\" id=\"tagInputContainer\">\n                    <div class=\"tag-input-tags\" id=\"selectedTags\"></div>\n                    <input type=\"text\" id=\"tagInput\" placeholder=\"Type a tag to filter stories...\"\n                        autocomplete=\"off\" maxlength=\"60\">\n                    <div class=\"autocomplete-dropdown\" id=\"autocompleteDropdown\"></div>\n                </div>\n                <button class=\"btn btn-primary\" id=\"searchBtn\" onclick=\"doSearch()\">Search</button>\n            </div>\n\n            <div id=\"resultsContainer\">\n                <p class=\"empty-state\">Enter tags above and click Search, or browse all stories.</p>\n            </div>\n        </div>\n\n        <style>\n            .browse-page { max-width: 960px; }\n\n            .search-interface {\n                display: flex;\n                gap: 12px;\n                align-items: flex-start;\n                margin-bottom: 32px;\n            }\n\n            .tag-input-container {\n                flex: 1;\n                position: relative;\n                background: var(--color-bg-raised);\n                border: 1px solid var(--color-border);\n                border-radius: 8px;\n                padding: 8px 12px;\n                display: flex;\n                flex-wrap: wrap;\n                gap: 6px;\n                align-items: center;\n                min-height: 44px;\n                cursor: text;\n            }\n            .tag-input-container:focus-within {\n                border-color: var(--color-accent);\n            }\n\n            .tag-input-tags {\n                display: flex;\n                flex-wrap: wrap;\n                gap: 6px;\n            }\n\n            .tag-input-pill {\n                display: inline-flex;\n                align-items: center;\n                gap: 4px;\n                font-size: 0.8rem;\n                padding: 3px 8px 3px 10px;\n                border-radius: 100px;\n                background: var(--color-tag-bg);\n                border: 1px solid var(--color-tag-border);\n                color: var(--color-text-muted);\n            }\n            .tag-input-pill .remove-tag {\n                cursor: pointer;\n                font-size: 1rem;\n                line-height: 1;\n                color: var(--color-text-dim);\n                padding: 0 2px;\n            }\n            .tag-input-pill .remove-tag:hover {\n                color: var(--color-accent);\n            }\n\n            #tagInput {\n                border: none;\n                background: transparent;\n                color: var(--color-text);\n                font-family: var(--font-body);\n                font-size: 0.9rem;\n                outline: none;\n                flex: 1;\n                min-width: 120px;\n            }\n\n            .autocomplete-dropdown {\n                display: none;\n                position: absolute;\n                top: calc(100% + 4px);\n                left: 0;\n                right: 0;\n                background: var(--color-bg-surface);\n                border: 1px solid var(--color-border);\n                border-radius: 8px;\n                max-height: 240px;\n                overflow-y: auto;\n                z-index: 50;\n            }\n            .autocomplete-dropdown.open { display: block; }\n            .autocomplete-item {\n                padding: 10px 14px;\n                font-size: 0.85rem;\n                color: var(--color-text-muted);\n                cursor: pointer;\n            }\n            .autocomplete-item:hover,\n            .autocomplete-item.active {\n                background: var(--color-bg-raised);\n                color: var(--color-text);\n            }\n            .autocomplete-item .ac-count {\n                float: right;\n                font-size: 0.75rem;\n                color: var(--color-text-dim);\n            }\n\n            /* Results */\n            .results-header {\n                display: flex;\n                justify-content: space-between;\n                align-items: center;\n                margin-bottom: 16px;\n            }\n            .results-header .result-count {\n                font-size: 0.85rem;\n                color: var(--color-text-dim);\n            }\n\n            .pagination {\n                display: flex;\n                gap: 6px;\n                justify-content: center;\n                margin-top: 24px;\n            }\n            .pagination button, .pagination span {\n                min-width: 36px;\n                text-align: center;\n            }\n\n            .no-results {\n                padding: 48px 0;\n                text-align: center;\n            }\n            .no-results p {\n                color: var(--color-text-dim);\n                font-size: 0.95rem;\n                font-style: italic;\n            }\n\n            .story-meta { font-size: 0.82rem; color: var(--color-text-dim); }\n            .story-title { font-size: 0.9rem; font-weight: 500; color: var(--color-link); }\n\n            @media (max-width: 680px) {\n                .search-interface { flex-direction: column; }\n                .search-interface .btn { width: 100%; justify-content: center; }\n                .tag-input-container { width: 100%; box-sizing: border-box; }\n            }\n        </style>\n\n        <script>\n            // ── State ──\n            let selectedTags = [];\n            let currentPage = 1;\n            let currentSort = 'created_at';\n            let currentOrder = 'desc';\n            let acIndex = -1;\n            let acItems = [];\n\n            // ── Init ──\n            renderSelectedTags();\n            doSearch();\n\n            // ── Tag input ──\n            const tagInput = document.getElementById('tagInput');\n            const dropdown = document.getElementById('autocompleteDropdown');\n\n            document.getElementById('tagInputContainer').addEventListener('click', () => tagInput.focus());\n\n            tagInput.addEventListener('input', async () => {\n                const q = tagInput.value.trim();\n                acIndex = -1;\n                if (q.length < 1) { dropdown.classList.remove('open'); return; }\n\n                try {\n                    const res = await fetch('/api/browse/autocomplete?q=' + encodeURIComponent(q));\n                    const data = await res.json();\n                    acItems = data.tags || [];\n\n                    if (!acItems.length) { dropdown.classList.remove('open'); return; }\n\n                    dropdown.innerHTML = acItems.map((t, i) =>\n                        '<div class=\"autocomplete-item\" data-index=\"' + i + '\" onclick=\"selectAutocomplete(' + i + ')\">' +\n                        esc(t.tag_name) +\n                        '<span class=\"ac-count\">' + t.story_count + '</span></div>'\n                    ).join('');\n                    dropdown.classList.add('open');\n                } catch { dropdown.classList.remove('open'); }\n            });\n\n            tagInput.addEventListener('keydown', (e) => {\n                if (e.key === 'ArrowDown') {\n                    e.preventDefault();\n                    acIndex = Math.min(acIndex + 1, acItems.length - 1);\n                    highlightAc();\n                } else if (e.key === 'ArrowUp') {\n                    e.preventDefault();\n                    acIndex = Math.max(acIndex - 1, 0);\n                    highlightAc();\n                } else if (e.key === 'Enter') {\n                    e.preventDefault();\n                    if (acIndex >= 0 && acItems[acIndex]) {\n                        selectAutocomplete(acIndex);\n                    } else if (tagInput.value.trim()) {\n                        doSearch();\n                    }\n                } else if (e.key === 'Escape') {\n                    dropdown.classList.remove('open');\n                }\n            });\n\n            document.addEventListener('click', (e) => {\n                if (!e.target.closest('.tag-input-container')) dropdown.classList.remove('open');\n            });\n\n            function highlightAc() {\n                dropdown.querySelectorAll('.autocomplete-item').forEach((el, i) => {\n                    el.classList.toggle('active', i === acIndex);\n                });\n            }\n\n            function selectAutocomplete(index) {\n                const tag = acItems[index];\n                if (!tag) return;\n                addTag(tag.tag_name);\n                tagInput.value = '';\n                dropdown.classList.remove('open');\n                acIndex = -1;\n            }\n\n            function addTag(name) {\n                if (selectedTags.length >= 5) {\n                    showLightbox('Tag Limit', 'You can filter on maximum 5 tags.', [\n                        { label: 'OK', className: 'btn-secondary' }\n                    ]);\n                    return;\n                }\n                if (selectedTags.includes(name)) return;\n                selectedTags.push(name);\n                renderSelectedTags();\n                updateUrl();\n            }\n\n            function removeTag(name) {\n                selectedTags = selectedTags.filter(t => t !== name);\n                renderSelectedTags();\n                updateUrl();\n            }\n\n            function renderSelectedTags() {\n                const container = document.getElementById('selectedTags');\n                container.innerHTML = selectedTags.map(t =>\n                    '<span class=\"tag-input-pill\">' + esc(t) +\n                    '<span class=\"remove-tag\" onclick=\"removeTag(\\'' + t.replace(/'/g, \"\\\\'\") + '\\')\">&times;</span></span>'\n                ).join('');\n            }\n\n            function updateUrl() {\n                const tagStr = selectedTags.map(t => encodeURIComponent(t)).join('&');\n                const newUrl = '/browse-stories/' + (tagStr ? '?t=' + tagStr : '');\n                history.replaceState(null, '', newUrl);\n            }\n\n            // ── Search ──\n            async function doSearch(page) {\n                currentPage = page || 1;\n                const container = document.getElementById('resultsContainer');\n                container.innerHTML = '<p class=\"empty-state\" style=\"font-style:italic;\">Searching...</p>';\n\n                const params = new URLSearchParams();\n                if (selectedTags.length) params.set('tags', selectedTags.join(','));\n                params.set('page', currentPage);\n                params.set('sort', currentSort);\n                params.set('order', currentOrder);\n\n                // Track search\n                if (typeof trackEvent === 'function') {\n                    trackEvent('search_chain', { meta: { tags: selectedTags } });\n                }\n\n                try {\n                    const res = await fetch('/api/browse/search?' + params.toString());\n                    const data = await res.json();\n                    container.innerHTML = data.html;\n                } catch {\n                    container.innerHTML = '<p class=\"empty-state\">Search failed. Please try again.</p>';\n                }\n            }\n\n            function sortBy(col) {\n                if (currentSort === col) {\n                    currentOrder = currentOrder === 'desc' ? 'asc' : 'desc';\n                } else {\n                    currentSort = col;\n                    currentOrder = 'desc';\n                }\n                doSearch(1);\n            }\n\n            // HTML escaper for inline use\n            function esc(s) {\n                const d = document.createElement('div');\n                d.textContent = s;\n                return d.innerHTML;\n            }\n        </script>\n    "}