-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch-fallback.js
More file actions
136 lines (121 loc) · 5.83 KB
/
Copy pathsearch-fallback.js
File metadata and controls
136 lines (121 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* ============================================================
360 — SEARCH FALLBACK V2
Tries Google CSE first. On quota/error falls back to Vyntr.
Results styled to match 360's glassmorphism UI exactly.
============================================================ */
(function () {
// Requests go through the vyntr-proxy Supabase edge function so the Vyntr
// API key stays server-side instead of shipping to every visitor's browser.
const VYNTR_URL = "https://wiswfpfsjiowtrdyqpxy.supabase.co/functions/v1/vyntr-proxy";
let cseWorking = true;
let vyntrActive = false;
/* Watch for CSE quota errors */
const observer = new MutationObserver(() => {
const errEl = document.querySelector(".gs-no-results-result, .gsc-results-wrapper-visible");
if ((errEl?.textContent || "").match(/INVALID_ACCESS|quota|error/i)) {
if (!vyntrActive) triggerVyntr();
}
});
observer.observe(document.body, { childList: true, subtree: true });
/* Intercept form submits when CSE is down */
document.addEventListener("submit", e => {
if (vyntrActive) {
e.preventDefault();
const q = document.querySelector("input.gsc-input, #search-input")?.value?.trim();
if (q) showVyntrResults(q);
}
});
/* On page load — check for ?q= and CSE failure */
window.addEventListener("load", () => {
const q = new URLSearchParams(window.location.search).get("q");
if (q && vyntrActive) showVyntrResults(q);
});
function triggerVyntr() {
vyntrActive = true;
cseWorking = false;
const q = new URLSearchParams(window.location.search).get("q")
|| document.querySelector("input.gsc-input")?.value?.trim();
if (q) showVyntrResults(q);
}
/* Inject styles */
const style = document.createElement("style");
style.textContent = `
#vyntr-results { max-width:680px; margin:20px auto; padding:0 16px; font-family:var(--font,system-ui); }
.vyntr-header { font-size:13px; color:var(--mut,#6b7280); margin-bottom:16px; display:flex; align-items:center; gap:8px; }
.vyntr-header strong { color:var(--a,#3b82f6); }
.vyntr-result {
background:rgba(255,255,255,.6); backdrop-filter:blur(10px);
border-radius:16px; padding:16px; margin-bottom:14px;
border:1px solid var(--br,rgba(148,163,184,.4)); transition:.2s;
text-decoration:none; display:block; color:inherit;
}
body.dark .vyntr-result { background:rgba(15,23,42,.7); border-color:rgba(255,255,255,.15); }
.vyntr-result:hover { border-color:var(--a,#3b82f6); transform:translateY(-1px); }
.vyntr-result-meta { display:flex; align-items:center; gap:6px; margin-bottom:5px; }
.vyntr-favicon { width:14px; height:14px; border-radius:2px; }
.vyntr-domain { font-size:12px; color:var(--mut,#6b7280); }
body.dark .vyntr-domain { color:var(--mutd,#a3a7b3); }
.vyntr-title { font-size:18px; font-weight:600; color:var(--a,#3b82f6); margin-bottom:4px; }
.vyntr-snippet { font-size:13px; color:var(--mut,#6b7280); line-height:1.6; }
body.dark .vyntr-snippet { color:var(--mutd,#a3a7b3); }
.vyntr-loading { text-align:center; padding:40px; color:var(--mut,#6b7280); font-size:14px; }
.vyntr-badge {
display:inline-flex; align-items:center; gap:5px; padding:3px 10px;
border-radius:999px; background:rgba(59,130,246,.1);
border:1px solid rgba(59,130,246,.25); font-size:11px; font-weight:600; color:var(--a,#3b82f6);
}
`;
document.head.appendChild(style);
async function showVyntrResults(query) {
/* Hide CSE results */
document.querySelector(".gsc-results-wrapper-visible, .gsc-above-wrapper-area, .gcsc-find-more-on-google-root")?.style?.setProperty("display","none","important");
let container = document.getElementById("vyntr-results");
if (!container) {
container = document.createElement("div");
container.id = "vyntr-results";
const shell = document.querySelector(".search-shell, .gcse-search, #search-wrap");
shell ? shell.after(container) : document.body.appendChild(container);
}
container.innerHTML = `<div class="vyntr-loading">🔍 Searching with Vyntr...</div>`;
try {
const res = await fetch(`${VYNTR_URL}?q=${encodeURIComponent(query)}`, {
headers: { "Accept": "application/json" }
});
if (!res.ok) throw new Error("Vyntr error " + res.status);
const data = await res.json();
renderVyntr(container, query, data);
} catch (err) {
container.innerHTML = `<div class="vyntr-loading">❌ Search unavailable right now. Try again later.</div>`;
}
}
function renderVyntr(container, query, data) {
const results = data.results || data.web?.results || [];
if (!results.length) {
container.innerHTML = `<div class="vyntr-loading">No results found for "<strong>${query}</strong>"</div>`;
return;
}
let html = `
<div class="vyntr-header">
<span class="vyntr-badge">⚡ Vyntr Search</span>
Results for <strong>${query}</strong>
</div>`;
results.forEach(r => {
const url = r.url || r.link || "#";
const title = (r.title || "").replace(/</g,"<");
const snippet = (r.snippet || r.description || r.content || "").replace(/</g,"<");
let domain = "";
try { domain = new URL(url).hostname.replace("www.",""); } catch {}
const favicon = `https://www.google.com/s2/favicons?domain=${domain}&sz=16`;
html += `
<a class="vyntr-result" href="${url}" target="_blank" rel="noopener noreferrer">
<div class="vyntr-result-meta">
<img class="vyntr-favicon" src="${favicon}" alt="" onerror="this.style.display='none'" />
<span class="vyntr-domain">${domain}</span>
</div>
<div class="vyntr-title">${title}</div>
${snippet ? `<div class="vyntr-snippet">${snippet}</div>` : ""}
</a>`;
});
container.innerHTML = html;
}
})();