-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add ipfs wikipedia search in p2p apps
- Loading branch information
1 parent
508ba57
commit 97a8079
Showing
7 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<meta | ||
name="description" | ||
content="A lightweight, open-source search interface for browsing Wikipedia over IPFS (InterPlanetary File System)." | ||
/> | ||
<link rel="icon" href="./static/assets/favicon.ico" sizes="32x32" /> | ||
<title>IPFS Wikipedia Search</title> | ||
<style> | ||
@import url("./static/styles.css"); | ||
</style> | ||
</head> | ||
<body> | ||
<div class="search-container"> | ||
<img | ||
src="./static/assets/wikipedia-on-ipfs.png" | ||
alt="Wikipedia on IPFS (credit: https://github.com/ipfs/distributed-wikipedia-mirror)" | ||
/> | ||
<form id="searchForm"> | ||
<input | ||
type="text" | ||
id="searchInput" | ||
placeholder="Search Wikipedia on IPFS..." | ||
autocomplete="off" | ||
required | ||
/> | ||
<button type="submit">Search</button> | ||
</form> | ||
<ul id="suggestionsList" class="suggestions"></ul> | ||
<div id="errorMessage"></div> | ||
</div> | ||
|
||
<div class="info-container"> | ||
<a href="https://github.com/p2plabsxyz/wiki.p2plabs.xyz">Source Code</a> | | ||
<a href="ipns://wiki.p2plabs.xyz">ipns://</a> | ||
<a href="hyper://wiki.p2plabs.xyz">hyper://</a> | ||
</div> | ||
|
||
<script src="./script.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
const searchInput = document.getElementById("searchInput"); | ||
const suggestionsList = document.getElementById("suggestionsList"); | ||
const searchForm = document.getElementById("searchForm"); | ||
const errorMessage = document.getElementById("errorMessage"); | ||
let debounceTimeout; | ||
|
||
/** | ||
* Detect if the browser supports the 'ipns://' protocol. | ||
* We do this by assigning a test URL to a hidden <a> element and checking its .protocol. | ||
*/ | ||
function supportsIpnsProtocol() { | ||
const testLink = document.createElement("a"); | ||
testLink.href = "ipns://test"; | ||
return testLink.protocol === "ipns:"; | ||
} | ||
|
||
/** | ||
* Returns the base URL prefix for Wikipedia on IPFS, depending on IPNS protocol support. | ||
* If the browser supports IPNS, use 'ipns://en.wikipedia-on-ipfs.org/'. | ||
* Otherwise, fall back to the public https gateway. | ||
*/ | ||
function getIpfsBaseUrl() { | ||
if (supportsIpnsProtocol()) { | ||
// Direct IPNS (peer-to-peer) | ||
return "ipns://en.wikipedia-on-ipfs.org/"; | ||
} else { | ||
// HTTP gateway fallback | ||
return "https://en-wikipedia--on--ipfs-org.ipns.dweb.link/"; | ||
} | ||
} | ||
|
||
// Helper: fallback formatting in case we don’t get a resolved title. | ||
// It capitalizes each word and replaces spaces with underscores. | ||
function formatQuery(query) { | ||
return query | ||
.trim() | ||
.split(" ") | ||
.filter((word) => word.length) | ||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||
.join("_"); | ||
} | ||
|
||
// Redirect to the final article link (IPNS or gateway). | ||
function navigateToArticle(query) { | ||
const formattedQuery = formatQuery(query); | ||
const finalUrl = getIpfsBaseUrl() + "wiki/" + formattedQuery; | ||
window.location.href = finalUrl; | ||
} | ||
|
||
/** | ||
* Use the Official Wikipedia API (over HTTPS) to find the canonical title for `query`. | ||
* Then we redirect to our IPFS version of that canonical title. | ||
* This step ensures that e.g. searching “computer science” | ||
* will get a properly capitalized official title. | ||
*/ | ||
function resolveQuery(query) { | ||
const apiUrl = | ||
"https://en.wikipedia.org/w/api.php?origin=*&action=query&format=json&titles=" + | ||
encodeURIComponent(query); | ||
|
||
return fetch(apiUrl) | ||
.then((response) => response.json()) | ||
.then((data) => { | ||
// If there's any normalization (e.g. “computer science” → “Computer science”), use it. | ||
let resolvedTitle = query; | ||
if (data.query.normalized && data.query.normalized.length > 0) { | ||
resolvedTitle = data.query.normalized[0].to; | ||
} | ||
|
||
// Check if the page exists | ||
const pages = data.query.pages; | ||
const pageKey = Object.keys(pages)[0]; | ||
if (pages[pageKey].missing !== undefined) { | ||
// The page does not exist | ||
return null; | ||
} | ||
return resolvedTitle; | ||
}); | ||
} | ||
|
||
/** | ||
* Returns suggestions from Wikipedia on IPFS | ||
*/ | ||
function fetchSuggestions(query) { | ||
const apiUrl = 'https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&format=json&search=' + encodeURIComponent(query); | ||
|
||
fetch(apiUrl) | ||
.then((response) => response.json()) | ||
.then((data) => { | ||
// data format: [searchTerm, [suggestions], [descriptions], [links]] | ||
const suggestions = data[1]; | ||
renderSuggestions(suggestions); | ||
}) | ||
.catch((error) => { | ||
console.error("Error fetching suggestions:", error); | ||
suggestionsList.innerHTML = ""; | ||
}); | ||
} | ||
|
||
// Display the list of suggestions as clickable items. | ||
function renderSuggestions(suggestions) { | ||
suggestionsList.innerHTML = ""; | ||
if (!suggestions || suggestions.length === 0) { | ||
return; | ||
} | ||
suggestions.forEach((suggestion) => { | ||
const li = document.createElement("li"); | ||
li.textContent = suggestion; | ||
|
||
li.addEventListener("click", () => { | ||
// When a suggestion is clicked, we first do a canonical resolution: | ||
resolveQuery(suggestion).then((resolvedTitle) => { | ||
if (resolvedTitle) { | ||
const finalTitle = resolvedTitle.replace(/ /g, "_"); | ||
const finalUrl = getIpfsBaseUrl() + "wiki/" + finalTitle; | ||
window.location.href = finalUrl; | ||
} else { | ||
// If no canonical resolution, just attempt the fallback format | ||
navigateToArticle(suggestion); | ||
} | ||
}); | ||
}); | ||
|
||
suggestionsList.appendChild(li); | ||
}); | ||
} | ||
|
||
// Listen for input events and fetch suggestions (debounced). | ||
searchInput.addEventListener("input", () => { | ||
const query = searchInput.value; | ||
clearTimeout(debounceTimeout); | ||
|
||
if (query.trim().length < 3) { | ||
suggestionsList.innerHTML = ""; | ||
return; | ||
} | ||
|
||
debounceTimeout = setTimeout(() => { | ||
fetchSuggestions(query); | ||
}, 300); | ||
}); | ||
|
||
// Handle the form submission: canonicalize via the official Wikipedia API, then go to IPFS link. | ||
searchForm.addEventListener("submit", (e) => { | ||
e.preventDefault(); | ||
errorMessage.textContent = ""; | ||
|
||
const query = searchInput.value.trim(); | ||
if (!query) return; | ||
|
||
const button = searchForm.querySelector("button"); | ||
button.textContent = "Loading..."; | ||
|
||
resolveQuery(query) | ||
.then((resolvedTitle) => { | ||
if (resolvedTitle) { | ||
const finalTitle = resolvedTitle.replace(/ /g, "_"); | ||
const finalUrl = getIpfsBaseUrl() + "wiki/" + finalTitle; | ||
window.location.href = finalUrl; | ||
} else { | ||
// Show an error if page not found and revert button text. | ||
errorMessage.textContent = `No Wikipedia article found for "${query}". Please check your spelling or try another term.`; | ||
button.textContent = "Search"; | ||
} | ||
}) | ||
.catch((err) => { | ||
console.error("Error resolving query:", err); | ||
errorMessage.textContent = `An error occurred while searching for "${query}". Please try again later.`; | ||
button.textContent = "Search"; | ||
}); | ||
}); |
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
html, | ||
body { | ||
margin: 0; | ||
padding: 0; | ||
height: 100%; | ||
font-family: Arial, sans-serif; | ||
background: #f9f9f9; | ||
} | ||
|
||
body { | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: center; | ||
align-items: center; | ||
} | ||
|
||
.search-container { | ||
background: #fff; | ||
padding: 20px 25px; | ||
border-radius: 8px; | ||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | ||
width: 320px; | ||
text-align: center; | ||
margin-bottom: 20px; | ||
margin-top: 30px; | ||
} | ||
.search-container img { | ||
max-width: 150px; | ||
height: auto; | ||
margin-bottom: 20px; | ||
} | ||
form { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: stretch; | ||
} | ||
input[type="text"] { | ||
padding: 10px; | ||
font-size: 16px; | ||
border: 1px solid #ccc; | ||
border-radius: 4px; | ||
box-sizing: border-box; | ||
} | ||
button { | ||
margin-top: 10px; | ||
padding: 10px; | ||
font-size: 16px; | ||
border: none; | ||
border-radius: 4px; | ||
background-color: #007bff; | ||
color: #fff; | ||
cursor: pointer; | ||
} | ||
button:hover { | ||
background-color: #0056b3; | ||
} | ||
|
||
.suggestions { | ||
margin-top: 10px; | ||
list-style: none; | ||
padding: 0; | ||
color: oklch(0.556 0 0); | ||
border: 1px solid #eee; | ||
border-radius: 4px; | ||
max-height: 200px; | ||
overflow-y: auto; | ||
text-align: left; | ||
scrollbar-width: none; /* Firefox */ | ||
-ms-overflow-style: none; /* IE 10+ */ | ||
} | ||
.suggestions::-webkit-scrollbar { | ||
display: none; /* Chrome, Safari, Opera */ | ||
} | ||
.suggestions li { | ||
padding: 8px 10px; | ||
border-bottom: 1px solid #eee; | ||
cursor: pointer; | ||
} | ||
.suggestions li:hover { | ||
background: #f0f0f0; | ||
} | ||
.suggestions li:last-child { | ||
border-bottom: none; | ||
} | ||
|
||
#errorMessage { | ||
margin-top: 10px; | ||
color: oklch(0.708 0 0); | ||
font-size: 14px; | ||
min-height: 20px; | ||
} | ||
|
||
.info-container { | ||
font-size: 12px; | ||
color: oklch(0.556 0 0); | ||
margin-bottom: 20px; | ||
text-align: center; | ||
} | ||
.info-container a { | ||
text-decoration: underline dotted; | ||
color: oklch(0.556 0 0); | ||
} | ||
.info-container a:hover { | ||
text-decoration: none; | ||
} | ||
.search-container img:hover { | ||
animation: heartbeat 1s infinite; | ||
} | ||
@keyframes heartbeat { | ||
0% { | ||
transform: scale(1); | ||
} | ||
25% { | ||
transform: scale(1.1); | ||
} | ||
50% { | ||
transform: scale(1); | ||
} | ||
75% { | ||
transform: scale(1.1); | ||
} | ||
100% { | ||
transform: scale(1); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.