ipfs storage for images and other nontext items. for use with etica - runs on etica network and currencys https://collect.etica-stats.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

283 lines
9.3 KiB

// collect — frontend
const $ = (id) => document.getElementById(id);
const log = (msg) => {
const el = $("log");
if (!el) return;
const now = new Date().toISOString().replace("T"," ").replace("Z","");
el.textContent += `[${now}] ${msg}\n`;
el.scrollTop = el.scrollHeight;
};
let CFG = null;
let RATES = null;
let CURRENT_QUOTE = null;
let CURRENT_TX = null;
let SELECTED_ACCOUNT = null;
// ===== utils =====
function bytesHuman(n=0){
const u = ["B","KB","MB","GB","TB"];
let i = 0; let v = Number(n||0);
while (v >= 1024 && i < u.length-1){ v/=1024; i++; }
return `${v.toFixed(i?2:0)} ${u[i]}`;
}
function middleEllipsis(s="", keep=6){
s = String(s); if (s.length <= keep*2) return s;
return `${s.slice(0,keep)}${s.slice(-keep)}`;
}
async function api(path, opt={}){
const res = await fetch(path, { ...opt, headers: { "content-type":"application/json", ...(opt.headers||{}) }});
if (!res.ok) throw new Error(`${path} ${res.status}`);
return res.json();
}
function isImageFilename(name = "") {
const n = name.toLowerCase();
return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") ||
n.endsWith(".gif") || n.endsWith(".webp") || n.endsWith(".bmp") ||
n.endsWith(".svg");
}
function ipfsUrlFromPath(path) {
if (!path) return "";
const gw = (CFG && CFG.gateway) ? CFG.gateway.replace(/\/+$/,"") : "";
return gw ? `${gw}${path}` : path;
}
// ===== wallet UI =====
function updateWalletUI() {
const btn = $("btnConnectTop");
const acctEl = $("tbAcct");
if (!btn || !acctEl) return;
if (!SELECTED_ACCOUNT) {
btn.disabled = !window.ethereum;
btn.textContent = window.ethereum ? "MetaMask Login" : "No MetaMask";
acctEl.textContent = "wallet: not connected";
} else {
btn.disabled = false;
btn.textContent = "MetaMask Logout";
acctEl.textContent = `wallet: ${SELECTED_ACCOUNT}`;
}
}
$("btnConnectTop").addEventListener("click", async () => {
try {
if (!window.ethereum) { updateWalletUI(); return; }
if (!SELECTED_ACCOUNT) {
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
SELECTED_ACCOUNT = (accounts && accounts[0]) ? accounts[0] : null;
log(`✅ Login ${SELECTED_ACCOUNT || "(none)"}`);
updateWalletUI();
if (SELECTED_ACCOUNT) await loadFiles();
} else {
log("👋 Logout");
SELECTED_ACCOUNT = null;
CURRENT_QUOTE = null;
CURRENT_TX = null;
updateWalletUI();
await loadFiles();
}
} catch (e) {
log(`wallet toggle error: ${e.message}`);
}
});
// ===== config & health & rates =====
async function loadConfigAndHealth(){
CFG = await api("/api/config/public");
$("cfgChain").textContent = CFG.chain || "–";
$("cfgRpc").textContent = CFG.rpc || "–";
$("cfgPayTo").textContent = CFG.payTo || "–";
$("cfgGateway").textContent = CFG.gateway || "–";
$("cfgTier").textContent = (CFG.tierBytes ? `${Math.round(CFG.tierBytes/1024/1024)} MB` : "–");
$("cfgPpt").textContent = (CFG.priceEgazPerTier ? `${CFG.priceEgazPerTier} ${CFG.currencyBase||"EGAZ"}` : "–");
$("cfgConf").textContent = CFG.requiredConfirmations ?? "–";
const h = await api("/api/health");
$("healthBlock").textContent = h.latestBlock ?? "–";
$("healthReq").textContent = h.requiredConfirmations ?? "–";
$("healthNow").textContent = h.now ?? "–";
try{
RATES = await api("/api/rates");
$("rateSrc").textContent = RATES.source || "fallback";
$("rateEgazUsd").textContent = RATES.egazUsd ? `$${RATES.egazUsd}` : "–";
$("rateEtiUsd").textContent = RATES.etiUsd ? `$${RATES.etiUsd}` : "–";
$("rateEtiPerEgaz").textContent = RATES.etiPerEgaz ? `${RATES.etiPerEgaz.toFixed(6)} ETI` : "–";
log(`Rates src=${RATES.source||"?"} egazUsd=${RATES.egazUsd||0} etiUsd=${RATES.etiUsd||0}`);
}catch(e){
$("rateSrc").textContent = "unavailable";
$("rateEgazUsd").textContent = "–";
$("rateEtiUsd").textContent = "–";
$("rateEtiPerEgaz").textContent = "–";
log(`rates error: ${e.message}`);
}
}
// ===== files =====
async function loadFiles(){
const status = $("filesStatus");
try{
status.textContent = "loading…";
const addr = SELECTED_ACCOUNT || "0x0000000000000000000000000000000000000000";
const res = await api(`/api/files?address=${addr}`);
const files = res.files || [];
renderFiles(files);
renderGallery(files);
status.textContent = `showing ${files.length} file(s) for ${middleEllipsis(addr)}`;
// recent preview
const recent = files[0];
const box = $("recentBox");
const who = $("recentFor");
who.textContent = `Showing files for ${middleEllipsis(addr)}`;
if (!recent){ box.textContent = "No preview"; return; }
const href = ipfsUrlFromPath(recent.path || "");
if (href && isImageFilename(recent.filename||"")){
box.innerHTML = `<a href="${href}" target="_blank" rel="noopener"><img src="${href}" alt=""></a>`;
} else {
box.innerHTML = `<a href="${href}" target="_blank" rel="noopener" class="small" style="color:#98a2b3">${recent.filename||"file"}</a>`;
}
}catch(e){
status.textContent = `error: ${e.message}`;
renderFiles([]);
renderGallery([]);
}
}
function renderFiles(files){
const wrap = $("filesTableWrap");
const empty = $("filesEmpty");
const tbody = $("filesTbody");
tbody.innerHTML = "";
if (!files || files.length === 0){
empty.classList.remove("hidden");
wrap.classList.add("hidden");
return;
}
empty.classList.add("hidden");
wrap.classList.remove("hidden");
for (const f of files){
const tr = document.createElement("tr");
const size = f.size_bytes ?? f.sizeBytes ?? 0;
const cid = f.cid || "";
const path = f.path || "";
const label = f.label || "";
const when = f.created_at || f.createdAt || "";
const status = f.status || "stored";
tr.innerHTML = `
<td>${f.filename || "—"}</td>
<td>${bytesHuman(size)}</td>
<td class="mono small">${cid || "—"}</td>
<td class="mono small">${path || "—"}</td>
<td>${label || "—"}</td>
<td><span class="tag">${status}</span></td>
<td class="small">${when ? new Date(when).toLocaleString() : "—"}</td>
`;
tbody.appendChild(tr);
}
}
function renderGallery(files){
const wrap = $("gallery");
const status = $("galleryStatus");
wrap.innerHTML = "";
if (!files || files.length === 0){
status.textContent = "No files yet.";
return;
}
status.textContent = `Showing ${files.length} file(s).`;
for (const f of files){
const card = document.createElement("div");
card.className = "card-thumb";
const head = document.createElement("div");
head.className = "small";
head.style.display = "flex";
head.style.justifyContent = "space-between";
head.style.gap = "8px";
head.innerHTML = `
<span class="mono" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;">${f.filename || "—"}</span>
<span class="tag">${(f.currency || "EGAZ").toUpperCase()}</span>
`;
const media = document.createElement("div");
media.className = "thumb-box";
const link = document.createElement("a");
const href = ipfsUrlFromPath(f.path || "");
link.href = href || "#";
link.target = "_blank";
link.rel = "noopener";
if (href && isImageFilename(f.filename || "")) {
const img = document.createElement("img");
img.src = href;
img.alt = f.filename || "image";
img.loading = "lazy";
img.onerror = () => {
media.innerHTML = `<div class="small" style="color:#98a2b3;text-align:center;padding:12px;">
Preview unavailable<br/><span class="mono">${(f.path||"").slice(0,40)}…</span>
</div>`;
};
link.appendChild(img);
} else {
link.innerHTML = `<div class="small" style="color:#98a2b3;text-align:center;padding:12px;">
No preview<br/><span class="mono">${(f.path||"").slice(0,40)}…</span>
</div>`;
}
media.appendChild(link);
const foot = document.createElement("div");
foot.className = "small";
foot.style.display = "flex";
foot.style.justifyContent = "space-between";
const size = f.size_bytes ?? f.sizeBytes ?? 0;
const when = f.created_at || f.createdAt || "";
foot.innerHTML = `
<span>${bytesHuman(size)}</span>
<span>${when ? new Date(when).toLocaleString() : ""}</span>
`;
card.appendChild(head);
card.appendChild(media);
card.appendChild(foot);
wrap.appendChild(card);
}
}
// ===== wallet ensure =====
async function ensureWallet(){
updateWalletUI();
if (!window.ethereum) return;
const accts = await ethereum.request({ method: "eth_accounts" });
SELECTED_ACCOUNT = (accts && accts[0]) ? accts[0] : null;
updateWalletUI();
ethereum.removeAllListeners?.("accountsChanged");
ethereum.on?.("accountsChanged", async (accounts) => {
SELECTED_ACCOUNT = (accounts && accounts[0]) ? accounts[0] : null;
updateWalletUI();
await loadFiles();
});
ethereum.removeAllListeners?.("chainChanged");
ethereum.on?.("chainChanged", async (_chainId) => {
await loadConfigAndHealth();
await loadFiles();
});
}
// ===== init =====
(async function init(){
try{
updateWalletUI(); // immediate correct button label
await loadConfigAndHealth();
await ensureWallet();
await loadFiles();
}catch(e){
log(`init error: ${e.message}`);
}
})();