Skip to content

Commit

Permalink
feat: add ipfs wikipedia search in p2p apps
Browse files Browse the repository at this point in the history
  • Loading branch information
akhileshthite committed Feb 26, 2025
1 parent 508ba57 commit 97a8079
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 0 deletions.
44 changes: 44 additions & 0 deletions src/pages/p2p/wiki/index.html
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>
171 changes: 171 additions & 0 deletions src/pages/p2p/wiki/script.js
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 added src/pages/p2p/wiki/static/assets/favicon.ico
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.
125 changes: 125 additions & 0 deletions src/pages/p2p/wiki/static/styles.css
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);
}
}
1 change: 1 addition & 0 deletions src/pages/peer-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class PeerBar extends HTMLElement {
{ href: 'peersky://p2p/chat/', img: 'chat.svg', alt: 'Peersky Chat' },
{ href: 'peersky://p2p/upload/', img: 'upload.svg', alt: 'Peersky Upload' },
{ href: 'peersky://p2p/editor/', img: 'build.svg', alt: 'Peersky Build' },
{ href: 'peersky://p2p/wiki/', img: 'wikipedia.svg', alt: 'Peersky Wiki' },
{ href: 'https://reader.distributed.press/', img: 'people.svg', alt: 'Social Reader' }
];

Expand Down
3 changes: 3 additions & 0 deletions src/pages/static/assets/svg/wikipedia.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 97a8079

Please sign in to comment.