Kaynağa Gözat

Oasis release 0.4.6

psy 5 gün önce
ebeveyn
işleme
4f9eb9bbb8

+ 12 - 0
docs/CHANGELOG.md

@@ -13,6 +13,18 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.4.6 - 2025-08-24
+ 
+### Fixed
+
+ + Follow/Unfollow and Pledges (projects plugin).
+ + Karma SCORE (inhabitants plugin).
+ 
+### Changed
+
+- Activity.
+- Inhabitants.
+- Search.
 
 ## v0.4.5 - 2025-08-21
 

+ 45 - 17
src/backend/backend.js

@@ -872,22 +872,44 @@ router
   .get('/inhabitants', async (ctx) => {
     const filter = ctx.query.filter || 'all';
     const query = {
-      search: ctx.query.search || ''
+        search: ctx.query.search || ''
     };
     if (['CVs', 'MATCHSKILLS'].includes(filter)) {
-      query.location = ctx.query.location || '';
-      query.language = ctx.query.language || '';
-      query.skills = ctx.query.skills || '';
+        query.location = ctx.query.location || '';
+        query.language = ctx.query.language || '';
+        query.skills = ctx.query.skills || '';
     }
     const userId = SSBconfig.config.keys.id;
     const inhabitants = await inhabitantsModel.listInhabitants({
-      filter,
-      ...query
+        filter,
+        ...query
     });
-    const addresses = await bankingModel.listAddressesMerged();
+    const [addresses, karmaList] = await Promise.all([
+        bankingModel.listAddressesMerged(),
+        Promise.all(
+            inhabitants.map(async (u) => {
+                try {
+                    const { karmaScore } = await bankingModel.getBankingData(u.id);
+                    return { id: u.id, karmaScore: typeof karmaScore === 'number' ? karmaScore : 0 };
+                } catch {
+                    return { id: u.id, karmaScore: 0 };
+                }
+            })
+        )
+    ]);
     const addrMap = new Map(addresses.map(x => [x.id, x.address]));
-    const inhabitantsWithAddr = inhabitants.map(u => ({ ...u, ecoAddress: addrMap.get(u.id) || null }));
-    ctx.body = await inhabitantsView(inhabitantsWithAddr, filter, query, userId);
+    const karmaMap = new Map(karmaList.map(x => [x.id, x.karmaScore]));
+    let enriched = inhabitants.map(u => ({
+        ...u,
+        ecoAddress: addrMap.get(u.id) || null,
+        karmaScore: karmaMap.has(u.id)
+            ? karmaMap.get(u.id)
+            : (typeof u.karmaScore === 'number' ? u.karmaScore : 0)
+    }));
+    if (filter === 'TOP KARMA') {
+        enriched = enriched.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0));
+    }
+    ctx.body = await inhabitantsView(enriched, filter, query, userId);
   })
   .get('/inhabitant/:id', async (ctx) => {
     const id = ctx.params.id;
@@ -938,8 +960,9 @@ router
   })
   .get('/activity', async ctx => {
     const filter = ctx.query.filter || 'recent';
-    const actions = await activityModel.listFeed(filter);
     const userId = SSBconfig.config.keys.id;
+    try { await bankingModel.getUserEngagementScore(userId); } catch (_) {}
+    const actions = await activityModel.listFeed(filter);
     ctx.body = activityView(actions, filter, userId);
   })
   .get("/profile", async (ctx) => {
@@ -1351,15 +1374,20 @@ router
     ctx.body = await singleJobsView(job, filter);
   })
   .get('/projects', async (ctx) => {
-    const projectsMod = ctx.cookies.get("projectsMod") || 'on'
-    if (projectsMod !== 'on') { ctx.redirect('/modules'); return }
-    const filter = ctx.query.filter || 'ALL'
+    const projectsMod = ctx.cookies.get("projectsMod") || 'on';
+    if (projectsMod !== 'on') { ctx.redirect('/modules'); return; }
+    const filter = ctx.query.filter || 'ALL';
     if (filter === 'CREATE') {
-      ctx.body = await projectsView([], 'CREATE'); return
+      ctx.body = await projectsView([], 'CREATE');
+      return;
+    }
+    const modelFilter = (filter === 'BACKERS') ? 'ALL' : filter;
+    let projects = await projectsModel.listProjects(modelFilter);
+    if (filter === 'MINE') {
+      const userId = SSBconfig.config.keys.id;
+      projects = projects.filter(project => project.author === userId);
     }
-    const modelFilter = (filter === 'BACKERS') ? 'ALL' : filter
-    const projects = await projectsModel.listProjects(modelFilter)
-    ctx.body = await projectsView(projects, filter)
+    ctx.body = await projectsView(projects, filter);
   })
   .get('/projects/edit/:id', async (ctx) => {
     const id = ctx.params.id

+ 6 - 1
src/client/assets/translations/oasis_en.js

@@ -1077,6 +1077,12 @@ module.exports = {
     bankUbiReceived:      "UBI Received",
     bankTx:               "Tx",
     bankEpochShort:       "Epoch",
+    activityProjectFollow: "%OASIS% is now %ACTION% this project %PROJECT%",
+    activityProjectUnfollow: "%OASIS% is now %ACTION% this project %PROJECT%",
+    activityProjectPledged: "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%",
+    following: "FOLLOWING",
+    unfollowing: "UNFOLLOWING",
+    pledged: "PLEDGED",
     //reports
     reportsTitle: "Reports",
     reportsDescription: "Manage and track reports related to issues, bugs, abuses and content warnings in your network.",
@@ -1702,7 +1708,6 @@ module.exports = {
     projectStatusCANCELLED: "CANCELLED",
     projectPledgeTitle: "Back this project",
     projectPledgePlaceholder: "Amount in ECO",
-    projectPledgeButton: "Pledge",
     projectBounties: "Bounties",
     projectBountiesInputLabel: "Bounties (one per line: Title|Amount [ECO]|Description)",
     projectBountiesPlaceholder: "Fix UI bug|100|Link to issue\nWrite docs|250|Outline usage examples",

+ 6 - 1
src/client/assets/translations/oasis_es.js

@@ -1075,6 +1075,12 @@ module.exports = {
     bankUbiReceived:      "UBI recibida",
     bankTx:               "Tx",
     bankEpochShort:       "Época",
+    activityProjectFollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
+    activityProjectUnfollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
+    activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%",
+    following: "SIGUIENDO",
+    unfollowing: "DEJANDO DE SEGUIR",
+    pledged: "APORTADO",
     //reports
     reportsTitle: "Informes",
     reportsDescription: "Gestiona y realiza un seguimiento de los informes relacionados con problemas, errores, abusos y advertencias de contenido en tu red.",
@@ -1714,7 +1720,6 @@ module.exports = {
     projectStatusCANCELLED: "CANCELADO",
     projectPledgeTitle: "Apoya este proyecto",
     projectPledgePlaceholder: "Cantidad en ECO",
-    projectPledgeButton: "Prometer",
     projectBounties: "Recompensas",
     projectBountiesInputLabel: "Recompensas (una por línea: Título|Cantidad [ECO]|Descripción)",
     projectBountiesPlaceholder: "Arreglar error en UI|100|Enlace al problema\nEscribir documentación|250|Ejemplos de uso",

+ 6 - 1
src/client/assets/translations/oasis_eu.js

@@ -1076,6 +1076,12 @@ module.exports = {
     bankUbiReceived:      "UBI jasota",
     bankTx:               "Tx",
     bankEpochShort:       "Epoka",
+    activityProjectFollow: "%OASIS% orain proiektu hau %ACTION% ari da %PROJECT%",
+    activityProjectUnfollow: "%OASIS% orain proiektu hau %ACTION% ari da %PROJECT%",
+    activityProjectPledged: "%OASIS%(e)k %AMOUNT% %ACTION% du proiektuan %PROJECT%",
+    following: "JARRAITZEN",
+    unfollowing: "UTZI DU JARRAITZEA",
+    pledged: "KONPROMISOA",
     //reports
     reportsTitle: "Txostenak",
     reportsDescription: "Kudeatu eta jarraitu arazo, akats, gehiegikeri eta eduki-abisuei buruzko txostena zure sarean.",
@@ -1713,7 +1719,6 @@ module.exports = {
     projectStatusCANCELLED: "EZEZTATUTA",
     projectPledgeTitle: "Proiektua babestu",
     projectPledgePlaceholder: "Kantitatea ECOtan",
-    projectPledgeButton: "Babestu",
     projectBounties: "Saria",
     projectBountiesInputLabel: "Sariak (lerro bakoitzean: Izenburua|Kantitatea [ECO]|Deskribapena)",
     projectBountiesPlaceholder: "UI akatsa konpondu|100|Arazoaren esteka\nDokumentuak idatzi|250|Erabilera adibideak",

+ 66 - 74
src/models/activity_model.js

@@ -4,15 +4,9 @@ const logLimit = getConfig().ssbLogStream?.limit || 1000;
 
 const N = s => String(s || '').toUpperCase().replace(/\s+/g, '_');
 const ORDER_MARKET = ['FOR_SALE','OPEN','RESERVED','CLOSED','SOLD'];
-const SCORE_MARKET = s => {
-  const i = ORDER_MARKET.indexOf(N(s));
-  return i < 0 ? -1 : i;
-};
 const ORDER_PROJECT = ['CANCELLED','PAUSED','ACTIVE','COMPLETED'];
-const SCORE_PROJECT = s => {
-  const i = ORDER_PROJECT.indexOf(N(s));
-  return i < 0 ? -1 : i;
-};
+const SCORE_MARKET = s => { const i = ORDER_MARKET.indexOf(N(s)); return i < 0 ? -1 : i };
+const SCORE_PROJECT = s => { const i = ORDER_PROJECT.indexOf(N(s)); return i < 0 ? -1 : i };
 
 function inferType(c = {}) {
   if (c.type === 'wallet' && c.coin === 'ECO' && typeof c.address === 'string') return 'bankWallet';
@@ -23,21 +17,14 @@ function inferType(c = {}) {
 
 module.exports = ({ cooler }) => {
   let ssb;
-  const openSsb = async () => {
-    if (!ssb) ssb = await cooler.open();
-    return ssb;
-  };
-  const hasBlob = async (ssbClient, url) => {
-    return new Promise((resolve) => {
-      ssbClient.blobs.has(url, (err, has) => {
-        resolve(!err && has);
-      });
-    });
-  };
+  const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb };
+  const hasBlob = async (ssbClient, url) => new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has)));
+
   return {
     async listFeed(filter = 'all') {
       const ssbClient = await openSsb();
       const userId = ssbClient.id;
+
       const results = await new Promise((resolve, reject) => {
         pull(
           ssbClient.createLogStream({ reverse: true, limit: logLimit }),
@@ -48,31 +35,20 @@ module.exports = ({ cooler }) => {
       const tombstoned = new Set();
       const parentOf = new Map();
       const idToAction = new Map();
+      const rawById = new Map();
 
       for (const msg of results) {
         const k = msg.key;
         const v = msg.value;
         const c = v?.content;
         if (!c?.type) continue;
-        if (c.type === 'tombstone' && c.target) {
-          tombstoned.add(c.target);
-          continue;
-        }
-        idToAction.set(k, {
-          id: k,
-          author: v?.author,
-          ts: v?.timestamp || 0,
-          type: inferType(c),
-          content: c
-        });
+        if (c.type === 'tombstone' && c.target) { tombstoned.add(c.target); continue }
+        idToAction.set(k, { id: k, author: v?.author, ts: v?.timestamp || 0, type: inferType(c), content: c });
+        rawById.set(k, msg);
         if (c.replaces) parentOf.set(k, c.replaces);
       }
 
-      const rootOf = (id) => {
-        let cur = id;
-        while (parentOf.has(cur)) cur = parentOf.get(cur);
-        return cur;
-      };
+      const rootOf = (id) => { let cur = id; while (parentOf.has(cur)) cur = parentOf.get(cur); return cur };
 
       const groups = new Map();
       for (const [id, action] of idToAction.entries()) {
@@ -86,34 +62,56 @@ module.exports = ({ cooler }) => {
       for (const [root, arr] of groups.entries()) {
         if (!arr.length) continue;
         const type = arr[0].type;
-        let tip;
-        if (type === 'market') {
-          tip = arr[0];
-          let bestScore = SCORE_MARKET(tip.content.status);
-          for (const a of arr) {
-            const s = SCORE_MARKET(a.content.status);
-            if (s > bestScore || (s === bestScore && a.ts > tip.ts)) {
-              tip = a; bestScore = s;
-            }
-          }
-        } else if (type === 'project') {
-          tip = arr[0];
-          let bestScore = SCORE_PROJECT(tip.content.status);
-          for (const a of arr) {
-            const s = SCORE_PROJECT(a.content.status);
-            if (s > bestScore || (s === bestScore && a.ts > tip.ts)) {
-              tip = a; bestScore = s;
-            }
-          }
-        } else {
-          tip = arr.reduce((best, a) => (a.ts > best.ts ? a : best), arr[0]);
+
+        if (type !== 'project') {
+          const tip = arr.reduce((best, a) => (a.ts > best.ts ? a : best), arr[0]);
+          for (const a of arr) idToTipId.set(a.id, tip.id);
+          continue;
         }
-        if (tombstoned.has(tip.id)) {
-          const nonTomb = arr.filter(a => !tombstoned.has(a.id));
-          if (!nonTomb.length) continue;
-          tip = nonTomb.reduce((best, a) => (a.ts > best.ts ? a : best), nonTomb[0]);
+
+        let tip = arr[0];
+        let bestScore = SCORE_PROJECT(tip.content.status);
+        for (const a of arr) {
+          const s = SCORE_PROJECT(a.content.status);
+          if (s > bestScore || (s === bestScore && a.ts > tip.ts)) { tip = a; bestScore = s }
         }
         for (const a of arr) idToTipId.set(a.id, tip.id);
+
+        const baseTitle = (tip.content && tip.content.title) || '';
+        const overlays = arr
+          .filter(a => a.type === 'project' && (a.content.followersOp || a.content.backerPledge))
+          .sort((a, b) => (a.ts || 0) - (b.ts || 0));
+
+        for (const ev of overlays) {
+          if (tombstoned.has(ev.id)) continue;
+
+          let kind = null;
+          let amount = null;
+
+          if (ev.content.followersOp === 'follow') kind = 'follow';
+          else if (ev.content.followersOp === 'unfollow') kind = 'unfollow';
+
+          if (ev.content.backerPledge && typeof ev.content.backerPledge.amount !== 'undefined') {
+            const amt = Math.max(0, parseFloat(ev.content.backerPledge.amount || 0) || 0);
+            if (amt > 0) { kind = kind || 'pledge'; amount = amt }
+          }
+
+          if (!kind) continue;
+
+          const augmented = {
+            ...ev,
+            type: 'project',
+            content: {
+              ...ev.content,
+              title: baseTitle,
+              projectId: tip.id,
+              activity: { kind, amount },
+              activityActor: ev.author
+            }
+          };
+          idToAction.set(ev.id, augmented);
+          idToTipId.set(ev.id, ev.id); 
+        }
       }
 
       const latest = [];
@@ -156,22 +154,16 @@ module.exports = ({ cooler }) => {
       deduped = Array.from(byKey.values());
 
       let out;
-      if (filter === 'mine') {
-        out = deduped.filter(a => a.author === userId);
-      } else if (filter === 'recent') {
-        const cutoff = Date.now() - 24 * 60 * 60 * 1000;
-        out = deduped.filter(a => (a.ts || 0) >= cutoff);
-      } else if (filter === 'all') {
-        out = deduped;
-      } else if (filter === 'banking') {
-        out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim');
-      } else if (filter === 'karma') {
-        out = deduped.filter(a => a.type === 'karmaScore');
-      } else {
-        out = deduped.filter(a => a.type === filter);
-      }
+      if (filter === 'mine') out = deduped.filter(a => a.author === userId);
+      else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff) }
+      else if (filter === 'all') out = deduped;
+      else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim');
+      else if (filter === 'karma') out = deduped.filter(a => a.type === 'karmaScore');
+      else out = deduped.filter(a => a.type === filter);
+
       out.sort((a, b) => (b.ts || 0) - (a.ts || 0));
       return out;
     }
   };
 };
+

+ 29 - 34
src/models/banking_model.js

@@ -162,7 +162,6 @@ module.exports = ({ services } = {}) => {
   };
 
   async function openSsb() {
-    if (services?.ssb) return services.ssb;
     if (services?.cooler?.open) return services.cooler.open();
     if (global.ssb) return global.ssb;
     try {
@@ -171,7 +170,7 @@ module.exports = ({ services } = {}) => {
       if (srv?.server) return srv.server;
       if (srv?.default) return srv.default;
     } catch (_) {}
-    return null;
+    return null; 
   }
 
   async function getWalletFromSSB(userId) {
@@ -412,19 +411,11 @@ module.exports = ({ services } = {}) => {
 
 async function publishKarmaScore(userId, karmaScore) {
   const ssb = await openSsb();
-  if (!ssb) return false;
+  if (!ssb || !ssb.publish) return false;
   const timestamp = new Date().toISOString();
-  const content = {
-    type: "karmaScore",
-    karmaScore,
-    userId: userId,
-    timestamp: timestamp,
-  };
+  const content = { type: "karmaScore", karmaScore, userId, timestamp };
   return new Promise((resolve, reject) => {
-    ssb.publish(content, (err, msg) => {
-      if (err) reject(err);
-      else resolve(msg);
-    });
+    ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
   });
 }
 
@@ -479,45 +470,49 @@ function scoreFromActions(actions) {
 }
 
 async function getUserEngagementScore(userId) {
-  const actions = await fetchUserActions(userId);
+  const ssb = await openSsb();
+  const uid = resolveUserId(userId);
+  const actions = await fetchUserActions(uid);
   const karmaScore = scoreFromActions(actions);
-  const previousKarmaScore = await getLastKarmaScore(userId);
-  const lastPublishedTimestamp = await getLastPublishedTimestamp(userId);
-  const currentTimestamp = Date.now();
-  const timeDifference = currentTimestamp - new Date(lastPublishedTimestamp).getTime();
-  const shouldPublish = karmaScore !== previousKarmaScore && timeDifference >= 24 * 60 * 60 * 1000;
-  const canPublish = Boolean(services?.ssb || global.ssb);
-  if (shouldPublish && canPublish) {
-    await publishKarmaScore(userId, karmaScore);
+
+  const prev = await getLastKarmaScore(uid);
+  const lastPublishedTimestamp = await getLastPublishedTimestamp(uid);
+
+  const isSelf = idsEqual(uid, ssb.id);
+  const hasSSB = !!(ssb && ssb.publish);
+
+  const changed = (prev === null) || (karmaScore !== prev); 
+  const nowMs = Date.now();
+  const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0;
+  const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000;
+
+  if (isSelf && hasSSB && changed && cooldownOk) {
+    await publishKarmaScore(uid, karmaScore);
   }
   return karmaScore;
 }
 
 async function getLastKarmaScore(userId) {
   const ssb = await openSsb();
-  if (!ssb) return 0;
-  const matchOne = (arr) => {
-    if (!arr || !arr.length) return 0;
-    const v = arr[0].value || arr[0];
-    const c = v.content || {};
-    return Number(c.karmaScore) || 0;
-  };
+  if (!ssb) return null;
   return new Promise((resolve) => {
     const source = ssb.messagesByType
       ? ssb.messagesByType({ type: "karmaScore", reverse: true })
       : ssb.createLogStream && ssb.createLogStream({ reverse: true });
-    if (!source) return resolve(0);
+    if (!source) return resolve(null);
     pull(
       source,
-      pull.filter((msg) => {
+      pull.filter(msg => {
         const v = msg.value || msg;
         const c = v.content || {};
         return c && c.type === "karmaScore" && c.userId === userId;
       }),
       pull.take(1),
       pull.collect((err, arr) => {
-        if (err) return resolve(0);
-        resolve(matchOne(arr));
+        if (err || !arr || !arr.length) return resolve(null);
+        const v = arr[0].value || arr[0];
+        const c = v.content || {};
+        resolve(Number(c.karmaScore) || 0);
       })
     );
   });
@@ -534,7 +529,7 @@ async function getLastPublishedTimestamp(userId) {
     if (!source) return resolve(fallback);
     pull(
       source,
-      pull.filter((msg) => {
+      pull.filter(msg => {
         const v = msg.value || msg;
         const c = v.content || {};
         return c && c.type === "karmaScore" && c.userId === userId;

+ 1 - 1
src/models/inhabitants_model.js

@@ -165,7 +165,7 @@ module.exports = ({ cooler }) => {
       if (filter === 'CVs' || filter === 'MATCHSKILLS') {
         const records = await new Promise((res, rej) => {
           pull(
-            ssbClient.createLogStream({ limit: logLimit }),
+            ssbClient.createLogStream({ limit: logLimit, reverse: true}),
             pull.filter(msg =>
               msg.value.content?.type === 'curriculum' &&
               msg.value.content?.type !== 'tombstone'

+ 27 - 43
src/models/projects_model.js

@@ -144,7 +144,6 @@ module.exports = ({ cooler }) => {
     async updateProject(id, patch) {
       const ssbClient = await openSsb()
       const current = await getById(id)
-      if (current.author !== ssbClient.id) throw new Error('Unauthorized')
 
       let blobId = (patch.image === undefined ? current.image : patch.image)
       blobId = extractBlobId(blobId)
@@ -211,54 +210,39 @@ module.exports = ({ cooler }) => {
     },
 
     async followProject(id, userId) {
-      const tip = await this.getProjectTipId(id)
-      const project = await this.getProjectById(tip)
-      const followers = Array.isArray(project.followers) ? project.followers.slice() : []
-      if (!followers.includes(userId)) followers.push(userId)
-      return this.updateProject(tip, { followers })
+      const tip = await this.getProjectTipId(id);
+      const project = await this.getProjectById(tip);
+      const followers = Array.isArray(project.followers) ? project.followers.slice() : [];
+      if (!followers.includes(userId)) followers.push(userId);
+      const activity = {
+        kind: 'follow',
+        amount: null,
+        activityActor: userId
+      };
+      return this.updateProject(tip, { followers, activity });
     },
 
     async unfollowProject(id, userId) {
-      const tip = await this.getProjectTipId(id)
-      const project = await this.getProjectById(tip)
-      const followers = (project.followers || []).filter(uid => uid !== userId)
-      return this.updateProject(tip, { followers })
+      const tip = await this.getProjectTipId(id);
+      const project = await this.getProjectById(tip);
+      const followers = (project.followers || []).filter(uid => uid !== userId);
+      const activity = {
+        kind: 'unfollow',
+        amount: null,
+        activityActor: userId
+      };
+      return this.updateProject(tip, { followers, activity });
     },
 
     async pledgeToProject(id, userId, amount) {
-      openSsb().then(ssbClient => {
-        const tip = getProjectTipId(id);
-        getProjectById(tip).then(project => {
-          const amt = Math.max(0, parseFloat(amount || 0) || 0);
-          if (amt <= 0) throw new Error('Invalid amount');     
-          const backers = Array.isArray(project.backers) ? project.backers.slice() : [];
-          backers.push({ userId, amount: amt, at: new Date().toISOString() });    
-          const pledged = (parseFloat(project.pledged || 0) || 0) + amt;  
-          updateProject(tip, { backers, pledged }).then(updated => {
-            if (project.author == userId) {
-              const recipients = [project.author];
-              const content = {
-               type: 'post',
-               from: ssbClient.id,
-               to: recipients,
-               subject: 'PROJECT_PLEDGE',
-               text: `${userId} has pledged ${amt} ECO to your project "${project.title}" /projects/${encodeURIComponent(tip)}`,
-               sentAt: new Date().toISOString(),
-               private: true,
-               meta: {
-                 type: 'project-pledge',
-                 projectId: tip,
-                 projectTitle: project.title,
-                 amount: amt,
-                 pledgedBy: userId
-               }
-             };
-             ssbClient.private.publish(content, recipients);
-            }
-           return updated;
-          });
-        });
-     });
+      const tip = await this.getProjectTipId(id);
+      const project = await this.getProjectById(tip);
+      const amt = Math.max(0, parseFloat(amount || 0) || 0);
+      if (amt <= 0) throw new Error('Invalid amount');
+      const backers = Array.isArray(project.backers) ? project.backers.slice() : [];
+      backers.push({ userId, amount: amt, at: new Date().toISOString() });
+      const pledged = (parseFloat(project.pledged || 0) || 0) + amt;
+      await this.updateProject(tip, { backers, pledged });
     },
 
     async addBounty(id, bounty) {

Dosya farkı çok büyük olduğundan ihmal edildi
+ 19 - 3
src/models/search_model.js


+ 17 - 14
src/models/stats_model.js

@@ -26,7 +26,7 @@ module.exports = ({ cooler }) => {
   const types = [
     'bookmark','event','task','votes','report','feed','project',
     'image','audio','video','document','transfer','post','tribe',
-    'market','forum','job','aiExchange','karmaScore'
+    'market','forum','job','aiExchange'
   ];
 
   const getFolderSize = (folderPath) => {
@@ -144,6 +144,7 @@ module.exports = ({ cooler }) => {
     const content = {};
     const opinions = {};
     for (const t of types) {
+      if (t === 'karmaScore') continue;
       let vals = Array.from(tipOf[t].values()).map(v => v.content);
       if (t === 'forum') vals = vals.filter(c => !(c.root && tombTargets.has(c.root)));
       content[t] = vals.length || 0;
@@ -233,6 +234,20 @@ module.exports = ({ cooler }) => {
       .filter(p => N(p.status) === 'ACTIVE' && parseFloat(p.goal || 0) > 0)
       .map(p => (parseFloat(p.pledged || 0) / parseFloat(p.goal || 1)) * 100);
 
+    const projectsKPIs = {
+      total: projectVals.length,
+      active: prActive,
+      completed: prCompleted,
+      paused: prPaused,
+      cancelled: prCancelled,
+      ecoGoalTotal: sum(prGoals),
+      ecoPledgedTotal: sum(prPledged),
+      successRate: projectVals.length ? (prCompleted / projectVals.length) * 100 : 0,
+      avgProgress: prProgress.length ? (sum(prProgress) / prProgress.length) : 0,
+      medianProgress: median(prProgress),
+      activeFundingAvg: activeFundingRates.length ? (sum(activeFundingRates) / activeFundingRates.length) : 0
+    };
+
     const topAuthorsMap = new Map();
     for (const m of scopedMsgs) {
       const a = m.value.author;
@@ -288,19 +303,7 @@ module.exports = ({ cooler }) => {
         revenueECO,
         avgSoldPrice: soldPrices.length ? (sum(soldPrices) / soldPrices.length) : 0
       },
-      projectsKPIs: {
-        total: projectVals.length,
-        active: prActive,
-        completed: prCompleted,
-        paused: prPaused,
-        cancelled: prCancelled,
-        ecoGoalTotal: sum(prGoals),
-        ecoPledgedTotal: sum(prPledged),
-        successRate: projectVals.length ? (prCompleted / projectVals.length) * 100 : 0,
-        avgProgress: prProgress.length ? (sum(prProgress) / prProgress.length) : 0,
-        medianProgress: median(prProgress),
-        activeFundingAvg: activeFundingRates.length ? (sum(activeFundingRates) / activeFundingRates.length) : 0
-      },
+      projectsKPIs,
       usersKPIs: {
         totalInhabitants: inhabitants,
         topAuthors

+ 1 - 1
src/server/package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.4.5",
+  "version": "0.4.6",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {

+ 1 - 1
src/server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.4.5",
+  "version": "0.4.6",
   "description": "Oasis Social Networking Project Utopia",
   "repository": {
     "type": "git",

+ 102 - 56
src/views/activity_view.js

@@ -210,7 +210,7 @@ function renderActionCards(actions, userId) {
       );
     }
 
-    if (content.type === 'audio') {
+    if (type === 'audio') {
       const { url, mimeType, title } = content;
       cardBody.push(
         div({ class: 'card-section audio' },
@@ -468,65 +468,111 @@ function renderActionCards(actions, userId) {
     }
 
     if (type === 'project') {
-      const { title, status, progress, goal, pledged, deadline, followers, backers, milestones, bounty, bountyAmount, bounty_currency } = content;
-      const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
-      const displayStatus = String(status || 'ACTIVE').toUpperCase();
-      const followersCount = Array.isArray(followers) ? followers.length : 0;
-      const backersCount = Array.isArray(backers) ? backers.length : 0;
-      const backersTotal = sumAmounts(backers || []);
-      const msCount = Array.isArray(milestones) ? milestones.length : 0;
-      const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
-      const bountyVal = typeof bountyAmount !== 'undefined' ? bountyAmount : (typeof bounty === 'number' ? bounty : null);
-      cardBody.push(
+      const {
+        title, status, progress, goal, pledged,
+        deadline, followers, backers, milestones,
+        bounty, bountyAmount, bounty_currency,
+        activity, activityActor
+    } = content;
+
+    const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
+    const displayStatus = String(status || 'ACTIVE').toUpperCase();
+    const followersCount = Array.isArray(followers) ? followers.length : 0;
+    const backersCount = Array.isArray(backers) ? backers.length : 0;
+    const backersTotal = sumAmounts(backers || []);
+    const msCount = Array.isArray(milestones) ? milestones.length : 0;
+    const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
+    const bountyVal = typeof bountyAmount !== 'undefined'
+        ? bountyAmount
+        : (typeof bounty === 'number' ? bounty : null);
+
+    if (activity && activity.kind) {
+        const tmpl =
+            activity.kind === 'follow'
+                ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
+                : activity.kind === 'unfollow'
+                    ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
+                    : '%OASIS% performed an unknown action on %PROJECT%';
+
+        const actionWord =
+            activity.kind === 'follow'
+                ? (i18n.following || 'FOLLOWING')
+                : activity.kind === 'unfollow'
+                    ? (i18n.unfollowing || 'UNFOLLOWING')
+                    : 'ACTION';
+
+        const msgHtml = tmpl
+            .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(activity.activityActor || '')}">${activity.activityActor || ''}</a>`)
+            .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
+            .replace('%ACTION%', `<strong>${actionWord}</strong>`);
+
+        return div({ class: 'card card-rpg' },
+            div({ class: 'card-header' },
+                h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
+                form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
+                    button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+                )
+            ),
+            div(
+                p({ innerHTML: msgHtml })
+            ),
+            p({ class: 'card-footer' },
+                span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
+                a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
+            )
+        );
+    }
+
+    cardBody.push(
         div({ class: 'card-section project' },
-          title ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.title + ':'), 
-            span({ class: 'card-value' }, title)
-          ) : "",
-          typeof goal !== 'undefined' ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectGoal + ':'), 
-            span({ class: 'card-value' }, `${goal} ECO`)
-          ) : "",
-          typeof progress !== 'undefined' ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectProgress + ':'), 
-            span({ class: 'card-value' }, `${progress || 0}%`)
-          ) : "",
-          deadline ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectDeadline + ':'), 
-            span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
-          ) : "",
-          div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectStatus + ':'), 
-            span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
-          ),
-          div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectFunding + ':'), 
-            span({ class: 'card-value' }, `${ratio}%`)
-          ),
-          typeof pledged !== 'undefined' ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectPledged + ':'), 
-            span({ class: 'card-value' }, `${pledged || 0} ECO`)
-          ) : "",
-          div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectFollowers + ':'), 
-            span({ class: 'card-value' }, `${followersCount}`)
-          ),
-          div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.projectBackers + ':'), 
-            span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
-          ),
-          msCount ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'), 
-            span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
-          ) : "",
-          bountyVal != null ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'), 
-            span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
-          ) : ""
+            title ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.title + ':'),
+                span({ class: 'card-value' }, title)
+            ) : "",
+            typeof goal !== 'undefined' ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectGoal + ':'),
+                span({ class: 'card-value' }, `${goal} ECO`)
+            ) : "",
+            typeof progress !== 'undefined' ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectProgress + ':'),
+                span({ class: 'card-value' }, `${progress || 0}%`)
+            ) : "",
+            deadline ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectDeadline + ':'),
+                span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
+            ) : "",
+            div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectStatus + ':'),
+                span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
+            ),
+            div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectFunding + ':'),
+                span({ class: 'card-value' }, `${ratio}%`)
+            ),
+            typeof pledged !== 'undefined' ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectPledged + ':'),
+                span({ class: 'card-value' }, `${pledged || 0} ECO`)
+            ) : "",
+            div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectFollowers + ':'),
+                span({ class: 'card-value' }, `${followersCount}`)
+            ),
+            div({ class: 'card-field' },
+                span({ class: 'card-label' }, i18n.projectBackers + ':'),
+                span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
+            ),
+            msCount ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
+                span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
+            ) : "",
+            bountyVal != null ? div({ class: 'card-field' },
+                span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
+                span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
+            ) : ""
         )
       );
     }
-
+      
     if (type === 'aiExchange') {
       const { ctx } = content;
       cardBody.push(

+ 29 - 27
src/views/projects_view.js

@@ -253,20 +253,21 @@ function renderMilestonesAndBounties(project, editable = false) {
   return div({ class: 'milestones-bounties' }, ...blocks, unassignedBlock);
 }
 
-const renderProjectList = (projects, filter) =>
+const renderProjectList = (projects, filter) => 
   projects.length > 0 ? projects.map(pr => {
-    const isMineFilter = String(filter).toUpperCase() === 'MINE'
-    const isAuthor = pr.author === userId
-    const statusUpper = String(pr.status || 'ACTIVE').toUpperCase()
-    const isActive = statusUpper === 'ACTIVE'
-    const pct = parseFloat(pr.progress || 0) || 0
-    const ratio = pr.goal ? Math.min(100, Math.round((parseFloat(pr.pledged || 0) / parseFloat(pr.goal)) * 100)) : 0
-    const mileDone = (pr.milestones || []).filter(m => m.done).length
-    const mileTotal = (pr.milestones || []).length
-    const statusClass = `status-${statusUpper.toLowerCase()}`
-    const remain = budgetSummary(pr).remaining
-    const followers = Array.isArray(pr.followers) ? pr.followers.length : 0
-    const backers = Array.isArray(pr.backers) ? pr.backers.length : 0
+    const isMineFilter = String(filter).toUpperCase() === 'MINE';
+    const isAuthor = pr.author === userId;
+    const statusUpper = String(pr.status || 'ACTIVE').toUpperCase();
+    const isActive = statusUpper === 'ACTIVE';
+    const pct = parseFloat(pr.progress || 0) || 0;
+    const ratio = pr.goal ? Math.min(100, Math.round((parseFloat(pr.pledged || 0) / parseFloat(pr.goal)) * 100)) : 0;
+    const mileDone = (pr.milestones || []).filter(m => m.done).length;
+    const mileTotal = (pr.milestones || []).length;
+    const statusClass = `status-${statusUpper.toLowerCase()}`;
+    const remain = budgetSummary(pr).remaining;
+    const followers = Array.isArray(pr.followers) ? pr.followers.length : 0;
+    const backers = Array.isArray(pr.backers) ? pr.backers.length : 0;
+
 
     return div({ class: `project-card ${statusClass}` },
       isMineFilter && isAuthor ? div({ class: "project-actions" },
@@ -290,18 +291,18 @@ const renderProjectList = (projects, filter) =>
           button({ class: "status-btn", type: "submit" }, i18n.projectSetProgress)
         )
       ) : null,
-
-      !isMineFilter && !isAuthor && isActive ? (Array.isArray(pr.followers) && pr.followers.includes(userId) ?
-        form({ method: "POST", action: `/projects/unfollow/${encodeURIComponent(pr.id)}` },
-          button({ type: "submit", class: "unsubscribe-btn" }, i18n.projectUnfollowButton)
-        ) :
-        form({ method: "POST", action: `/projects/follow/${encodeURIComponent(pr.id)}` },
-          button({ type: "submit", class: "subscribe-btn" }, i18n.projectFollowButton)
-        )
-      ) : null,
-
-      form({ method: "GET", action: `/projects/${encodeURIComponent(pr.id)}` },
-        button({ type: "submit", class: "filter-btn" }, i18n.viewDetailsButton)
+      div({ class: 'project-actions' },
+        !isMineFilter && !isAuthor && isActive ? (Array.isArray(pr.followers) && pr.followers.includes(userId) ?
+          form({ method: "POST", action: `/projects/unfollow/${encodeURIComponent(pr.id)}` },
+            button({ type: "submit", class: "unsubscribe-btn" }, i18n.projectUnfollowButton)
+          ) :
+          form({ method: "POST", action: `/projects/follow/${encodeURIComponent(pr.id)}` },
+            button({ type: "submit", class: "subscribe-btn" }, i18n.projectFollowButton)
+          )
+        ) : null,
+        form({ method: "GET", action: `/projects/${encodeURIComponent(pr.id)}` },
+          button({ type: "submit", class: "filter-btn" }, i18n.viewDetailsButton)
+        ),
       ),
       br(),
       h2(pr.title),
@@ -466,8 +467,9 @@ exports.singleProjectView = async (project, filter="ALL") => {
             button({ class: "status-btn", type: "submit" }, i18n.projectSetProgress)
           )
         ) : null,
-        (!isAuthor && Array.isArray(project.followers) && project.followers.includes(userId))
-          ? p({ class: 'hint' }, i18n.projectYouFollowHint) : null,
+	(!isAuthor && Array.isArray(project.followers) && project.followers.includes(userId)) 
+	  ? div({ class: 'hint' }, p({ class: 'hint' }, i18n.projectYouFollowHint)) 
+	  : null,
         h2(project.title),
         project.image ? div({ class: 'activity-image-preview' }, img({ src: `/blob/${encodeURIComponent(project.image)}` })) : null,
         field(i18n.projectDescription + ':', ''), p(...renderUrl(project.description)),

+ 428 - 369
src/views/search_view.js

@@ -14,31 +14,32 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
   searchInput.setAttribute("minlength", 3);
 
   const contentTypes = [
-    "post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes", 
-    "report", "task", "event", "bookmark", "image", "audio", "video", "document", "all"
+    "post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes",
+    "report", "task", "event", "bookmark", "image", "audio", "video", "document",
+    "bankWallet", "bankClaim", "project", "job", "forum", "vote", "contact", "pub", "all"
   ];
 
   const filterSelect = select(
-    { 
-      id: "search-type", 
-      name: "type", 
-      class: "input-select", 
-      style: "position:relative; z-index:10;" 
+    {
+      id: "search-type",
+      name: "type",
+      class: "input-select",
+      style: "position:relative; z-index:10;"
     },
     contentTypes.map(type =>
       option({
-        value: type === 'all' ? "" : type, 
+        value: type === 'all' ? "" : type,
         selected: (types.length === 0 && type === 'all') || types.includes(type)
       }, i18n[type + "Label"] || type.toUpperCase())
     )
   );
 
   const resultsPerPageSelect = select(
-    { 
-      id: "results-per-page", 
-      name: "resultsPerPage", 
-      class: "input-select", 
-      style: "position:relative; z-index:10;margin-left:10px;" 
+    {
+      id: "results-per-page",
+      name: "resultsPerPage",
+      class: "input-select",
+      style: "position:relative; z-index:10;margin-left:10px;"
     },
     option({ value: "100", selected: resultCount === "100" }, "100"),
     option({ value: "50", selected: resultCount === "50" }, "50"),
@@ -46,366 +47,425 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
     option({ value: "all", selected: resultCount === "all" }, i18n.allTypesLabel)
   );
 
-const getViewDetailsActionForSearch = (type, contentId) => {
-  switch (type) {
-    case 'votes': return `/votes/${encodeURIComponent(contentId)}`;
-    case 'transfer': return `/transfers/${encodeURIComponent(contentId)}`;
-    case 'tribe': return `/tribe/${encodeURIComponent(contentId)}`;
-    case 'curriculum': return `/inhabitant/${encodeURIComponent(contentId)}`;
-    case 'image': return `/images/${encodeURIComponent(contentId)}`;
-    case 'audio': return `/audios/${encodeURIComponent(contentId)}`;
-    case 'video': return `/videos/${encodeURIComponent(contentId)}`;
-    case 'document': return `/documents/${encodeURIComponent(contentId)}`;
-    case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`;
-    case 'event': return `/events/${encodeURIComponent(contentId)}`;
-    case 'task': return `/tasks/${encodeURIComponent(contentId)}`;
-    case 'post': return `/thread/${encodeURIComponent(contentId)}#${encodeURIComponent(contentId)}`;
-    case 'market': return `/market/${encodeURIComponent(contentId)}`;
-    case 'report': return `/reports/${encodeURIComponent(contentId)}`;
-    default: return '#';
-  }
-};
-
-let hasDocument = false; 
+  const getViewDetailsActionForSearch = (type, contentId, content) => {
+    switch (type) {
+      case 'votes': return `/votes/${encodeURIComponent(contentId)}`;
+      case 'transfer': return `/transfers/${encodeURIComponent(contentId)}`;
+      case 'tribe': return `/tribe/${encodeURIComponent(contentId)}`;
+      case 'curriculum': return `/inhabitant/${encodeURIComponent(contentId)}`;
+      case 'image': return `/images/${encodeURIComponent(contentId)}`;
+      case 'audio': return `/audios/${encodeURIComponent(contentId)}`;
+      case 'video': return `/videos/${encodeURIComponent(contentId)}`;
+      case 'document': return `/documents/${encodeURIComponent(contentId)}`;
+      case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`;
+      case 'event': return `/events/${encodeURIComponent(contentId)}`;
+      case 'task': return `/tasks/${encodeURIComponent(contentId)}`;
+      case 'post': return `/thread/${encodeURIComponent(contentId)}#${encodeURIComponent(contentId)}`;
+      case 'market': return `/market/${encodeURIComponent(contentId)}`;
+      case 'report': return `/reports/${encodeURIComponent(contentId)}`;
+      case 'project': return `/projects/${encodeURIComponent(contentId)}`;
+      case 'job': return `/jobs/${encodeURIComponent(contentId)}`;
+      case 'forum': return `/forum/${encodeURIComponent(contentId)}`;
+      case 'vote': return content && content.vote && content.vote.link ? `/thread/${encodeURIComponent(content.vote.link)}#${encodeURIComponent(content.vote.link)}` : '#';
+      case 'contact': return content && content.contact ? `/author/${encodeURIComponent(content.contact)}` : '#';
+      case 'pub': return '#';
+      case 'bankWallet': return `/banking`;
+      case 'bankClaim': return `/banking`;
+      default: return '#';
+    }
+  };
 
-const renderContentHtml = (content) => {
-  switch (content.type) {
-    case 'post':
-      return div({ class: 'search-post' },
-        content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null,
-        content.text ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: content.text })) : null
-      );
-    case 'about':
-      return div({ class: 'search-about' },
-        content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.name + ':'), span({ class: 'card-value' }, content.name)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.description + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.image ? img({ src: `/image/64/${encodeURIComponent(content.image)}` }) : null
-      );
-    case 'feed':
-      return div({ class: 'search-feed' },
-        content.text ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.text)) : null,
-        h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ':'), span({ class: 'card-value' }, content.refeeds))
-      );
-    case 'event':
-      return div({ class: 'search-event' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventDate + ':'), span({ class: 'card-value' }, new Date(content.date).toLocaleString())) : null,
-        content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventLocation + ':'), span({ class: 'card-value' }, content.location)) : null,
-        content.isPublic ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrivacyLabel + ':'), span({ class: 'card-value' }, content.isPublic)) : null, 
-        content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventStatus + ':'), span({ class: 'card-value' }, content.status)) : null,    
-        content.eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.eventUrl, target: '_blank' }, content.eventUrl))) : null,
-        content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrice + ':'), span({ class: 'card-value' }, content.price)) : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'votes':
-      const { question, deadline, status, votes, totalVotes } = content;
-      const votesList = votes && typeof votes === 'object'
-        ? Object.entries(votes).map(([option, count]) => ({ option, count }))
-        : [];
-      return div({ class: 'search-vote' },
-        br,
-        content.question ? div({ class: 'card-field' }, 
-          span({ class: 'card-label' }, i18n.voteQuestionLabel + ':'), 
-          span({ class: 'card-value' }, content.question)
-        ) : null,
-        content.status ? div({ class: 'card-field' }, 
-          span({ class: 'card-label' }, i18n.voteStatus + ':'), 
-          span({ class: 'card-value' }, content.status)
-        ) : null,
-        content.deadline ? div({ class: 'card-field' }, 
-          span({ class: 'card-label' }, i18n.voteDeadline + ':'), 
-          span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '')
-        ) : null,
-        div({ class: 'card-field' }, 
-          span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), 
-          span({ class: 'card-value' }, totalVotes !== undefined ? totalVotes : '0')
-        ),
-        br,
-        votesList.length > 0 ? div({ class: 'card-votes' }, 
-          table(
-            tr(...votesList.map(({ option }) => th(i18n[option] || option))),
-            tr(...votesList.map(({ count }) => td(count)))
-          )
-        ) : null
-      );
-    case 'tribe':
-      return div({ class: 'search-tribe' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.isAnonymous !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel + ':'), span({ class: 'card-value' }, content.isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : null,
-        content.inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeModeLabel + ':'), span({ class: 'card-value' }, content.inviteMode.toUpperCase())) : null,  
-        content.isLARP !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel + ':'), span({ class: 'card-value' }, content.isLARP ? i18n.tribeYes : i18n.tribeNo)) : null,
-        br,
-        content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }),
-        br,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.location + ':'), span({ class: 'card-value' }, content.location)) : null,
-        Array.isArray(content.members) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeMembersCount + ':'), span({ class: 'card-value' }, content.members.length)) : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'audio':
-      return content.url ? div({ class: 'search-audio' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
-        br,
-        audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' }),
-        br,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      ) : null;
-    case 'image':
-      return content.url ? div({ class: 'search-image' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : null,
-        br,
-        img({ src: `/blob/${encodeURIComponent(content.url)}` }),
-        br,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      ) : null;
-    case 'video':
-      return content.url ? div({ class: 'search-video' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
-        br,
-        videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360' }),
-        br,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      ) : null;
-    case 'document':
-      return div({ class: 'search-document' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        br,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
-        br,
-        div({
-          id: `pdf-container-${content.key || content.url}`,
-          class: 'card-field pdf-viewer-container',
-          'data-pdf-url': `/blob/${encodeURIComponent(content.url)}`
-        }),
-        br,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'market':
-      return div({ class: 'search-market' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, content.item_type.toUpperCase())) : null,
-        content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemCondition + ':'), span({ class: 'card-value' }, content.status)) : null,
-        content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemDeadline + ':'), span({ class: 'card-value' }, new Date(content.deadline).toLocaleString())) : null,
-        br,
-        content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}`, class: 'market-image' }) : null,
-        br,
-        content.seller ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemSeller + ':'), span({ class: 'card-value' }, a({ class: "user-link", href: `/author/${encodeURIComponent(content.seller)}` }, content.seller))) : null,
-        content.stock ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock || 'N/A')) : null, 
-        content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriceLabel + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null,    
-        content.condition ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.condition)) : null,
-        content.includesShipping ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemIncludesShipping + ':'), span({ class: 'card-value' }, `${content.includesShipping ? i18n.YESLabel : i18n.NOLabel}`)) : null,
-        content.auctions_poll && content.auctions_poll.length > 0
-          ? div({ class: 'auction-info' },
-              p(i18n.marketAuctionBids),
-              table({ class: 'auction-bid-table' },
-                tr(
-                  th(i18n.marketAuctionBidTime),
-                  th(i18n.marketAuctionUser),
-                  th(i18n.marketAuctionBidAmount)
-                ),
-                content.auctions_poll.map(bid => {
-                  const [userId, bidAmount, bidTime] = bid.split(':');
-                  return tr(
-                    td(moment(bidTime).format('YYYY-MM-DD HH:mm:ss')),
-                    td(a({ href: `/author/${encodeURIComponent(userId)}` }, userId)),
-                    td(`${parseFloat(bidAmount).toFixed(6)} ECO`)
-                  );
-                })
-              )
-          )
-          : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'bookmark':
-      return div({ class: 'search-bookmark' },
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.url ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.url, target: '_blank' }, content.url))) : null,
-        content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkCategory + ':'), span({ class: 'card-value' }, content.category)) : null,
-        content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())) : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'task':
-      return div({ class: 'search-task' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null,
-        content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
-        content.priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriorityLabel + ':'), span({ class: 'card-value' }, content.priority)) : null,
-        typeof content.isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchIsPublicLabel + ':'), span({ class: 'card-value' }, content.isPublic ? i18n.YESLabel : i18n.NOLabel)) : null,
-        content.startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskStartTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.startTime).toLocaleString())) : null,
-        content.endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskEndTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.endTime).toLocaleString())) : null,
-        Array.isArray(content.assignees) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskAssignees + ':'), span({ class: 'card-value' }, content.assignees.length)) : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'report':
-      return div({ class: 'search-report' },
-        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
-        content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
-        content.severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsSeverity + ':'), span({ class: 'card-value' }, content.severity)) : null,
-        content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
-        br,
-        content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}` }) : null,
-        br,
-        typeof content.confirmations === 'number' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsConfirmations + ':'), span({ class: 'card-value' }, content.confirmations)) : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'transfer':
-      return div({ class: 'search-transfer' },
-        content.concept ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConcept + ':'), span({ class: 'card-value' }, content.concept)) : null,
-        content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersDeadline + ':'), span({ class: 'card-value' }, content.deadline)) : null,      
-        content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersStatus + ':'), span({ class: 'card-value' }, content.status)) : null,
-        content.amount ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersAmount + ':'), span({ class: 'card-value' }, content.amount)) : null, 
-        br,
-        content.from ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersFrom + ':'), span({ class: 'card-value' }, a({ class: "user-link", href: `/author/${encodeURIComponent(content.from)}` }, content.from))) : null,
-        content.to ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersTo + ':'), span({ class: 'card-value' }, a({ class: "user-link", href: `/author/${encodeURIComponent(content.to)}` }, content.to))) : null,
-        br,
-        content.confirmedBy && content.confirmedBy.length
-          ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConfirmations + ':'), span({ class: 'card-value' }, content.confirmedBy.length))
-          : null,
-        content.tags && content.tags.length
-          ? div({ class: 'card-tags' }, content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
-      );
-    case 'curriculum':
-      return div({ class: 'search-curriculum' },
-        content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvNameLabel + ':'), span({ class: 'card-value' }, content.name)) : null,
-        content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
-        content.photo ? img({ src: `/blob/${encodeURIComponent(content.photo)}`, class: 'curriculum-photo' }) : null,
-        content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null,
-        content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
-        content.preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvPreferencesLabel + ':'), span({ class: 'card-value' }, content.preferences)) : null,
-        Array.isArray(content.personalSkills) && content.personalSkills.length
-          ? div({ class: 'card-field' }, content.personalSkills.map(skill =>
-              a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: 'tag-link' }, `#${skill}`)
-            )) : null,
-        Array.isArray(content.personalExperiences) && content.personalExperiences.length
-          ? div({ class: 'card-field' }, content.personalExperiences.map(exp => p(exp))) : null,
-        Array.isArray(content.oasisExperiences) && content.oasisExperiences.length
-          ? div({ class: 'card-field' }, content.oasisExperiences.map(exp => p(exp))) : null,
-        Array.isArray(content.oasisSkills) && content.oasisSkills.length
-          ? div({ class: 'card-field' }, content.oasisSkills.map(skill => p(skill))) : null,
-        Array.isArray(content.educationExperiences) && content.educationExperiences.length
-          ? div({ class: 'card-field' }, content.educationExperiences.map(exp => p(exp))) : null,
-        Array.isArray(content.educationalSkills) && content.educationalSkills.length
-          ? div({ class: 'card-field' }, content.educationalSkills.map(skill =>
-              a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: 'tag-link' }, `#${skill}`)
-            )) : null,
-        Array.isArray(content.languages) && content.languages.length
-          ? div({ class: 'card-field' }, content.languages.map(lang => p(lang))) : null,
-        Array.isArray(content.professionalExperiences) && content.professionalExperiences.length
-          ? div({ class: 'card-field' }, content.professionalExperiences.map(exp => p(exp))) : null,
-       Array.isArray(content.professionalSkills) && content.professionalSkills.length
-          ? div({ class: 'card-field' }, content.professionalSkills.map(skill => p(skill))) : null
-      );
-    default:
-      return div({ class: 'styled-text', innerHTML: renderTextWithStyles(content.text || content.description || content.title || '[no content]') });
-  }
-};
+  let hasDocument = false;
 
-const resultSection = Object.entries(results).length > 0
-  ? Object.entries(results).map(([key, msgs]) =>
-    div(
-      { class: "search-result-group" },
-      h2(i18n[key + "Label"] || key),
-      ...msgs.map((msg) => {
-        const content = msg.value.content || {};
-        const created = new Date(msg.timestamp).toLocaleString();
-        if (content.type === 'document') hasDocument = true;
-        const contentHtml = renderContentHtml(content);
-        let author;
-        let authorUrl = '#';
-        if (content.type === 'market') {
-          author = content.seller || i18n.anonymous || "Anonymous";
-          authorUrl = `/author/${encodeURIComponent(content.seller)}`;
-        } else if (content.type === 'event') {
-          author = content.organizer || i18n.anonymous || "Anonymous";
-          authorUrl = `/author/${encodeURIComponent(content.organizer)}`;
-        } else if (content.type === 'transfer') {
-          author = content.from || i18n.anonymous || "Anonymous";
-          authorUrl = `/author/${encodeURIComponent(content.from)}`;
-        } else if (content.type === 'post' || content.type === 'about') {
-          author = msg.value.author || i18n.anonymous || "Anonymous";
-          authorUrl = `/author/${encodeURIComponent(msg.value.author)}`;
-        } else if (content.type === 'report') {
-          author = content.author || i18n.anonymous || "Anonymous";
-          authorUrl = `/author/${encodeURIComponent(content.author || 'Anonymous')}`;
-        } else if (content.type === 'votes') {
-          author = content.createdBy || i18n.anonymous || "Anonymous";
-          authorUrl = `/author/${encodeURIComponent(content.createdBy || 'Anonymous')}`;   
-        } else {
-          author = content.author
-          authorUrl = `/author/${encodeURIComponent(content.author || 'Anonymous')}`;
-        }
-        
-        const contentId = msg.key;
-        const detailsButton = form({ method: "GET", action: getViewDetailsActionForSearch(content.type, contentId) },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+  const renderContentHtml = (content) => {
+    switch (content.type) {
+      case 'post':
+        return div({ class: 'search-post' },
+          content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null,
+          content.text ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: content.text })) : null
         );
-
-        return div({ class: 'result-item' }, [
-          detailsButton,
+      case 'about':
+        return div({ class: 'search-about' },
+          content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.name + ':'), span({ class: 'card-value' }, content.name)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.description + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.image ? img({ src: `/image/64/${encodeURIComponent(content.image)}` }) : null
+        );
+      case 'feed':
+        return div({ class: 'search-feed' },
+          content.text ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.text)) : null,
+          h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ':'), span({ class: 'card-value' }, content.refeeds))
+        );
+      case 'event':
+        return div({ class: 'search-event' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventDate + ':'), span({ class: 'card-value' }, new Date(content.date).toLocaleString())) : null,
+          content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventLocation + ':'), span({ class: 'card-value' }, content.location)) : null,
+          content.isPublic ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrivacyLabel + ':'), span({ class: 'card-value' }, content.isPublic)) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventStatus + ':'), span({ class: 'card-value' }, content.status)) : null,
+          content.eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.eventUrl, target: '_blank' }, content.eventUrl))) : null,
+          content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrice + ':'), span({ class: 'card-value' }, content.price)) : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'votes':
+        const votesList = content.votes && typeof content.votes === 'object'
+          ? Object.entries(content.votes).map(([option, count]) => ({ option, count }))
+          : [];
+        return div({ class: 'search-vote' },
+          br,
+          content.question ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.voteQuestionLabel + ':' ),
+            span({ class: 'card-value' }, content.question)
+          ) : null,
+          content.status ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.voteStatus + ':' ),
+            span({ class: 'card-value' }, content.status)
+          ) : null,
+          content.deadline ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.voteDeadline + ':' ),
+            span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '')
+          ) : null,
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.voteTotalVotes + ':' ),
+            span({ class: 'card-value' }, content.totalVotes !== undefined ? content.totalVotes : '0')
+          ),
+          br,
+          votesList.length > 0 ? div({ class: 'card-votes' },
+            table(
+              tr(...votesList.map(({ option }) => th(i18n[option] || option))),
+              tr(...votesList.map(({ count }) => td(count)))
+            )
+          ) : null
+        );
+      case 'tribe':
+        return div({ class: 'search-tribe' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.isAnonymous !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel + ':'), span({ class: 'card-value' }, content.isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : null,
+          content.inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeModeLabel + ':'), span({ class: 'card-value' }, content.inviteMode.toUpperCase())) : null,
+          content.isLARP !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel + ':'), span({ class: 'card-value' }, content.isLARP ? i18n.tribeYes : i18n.tribeNo)) : null,
+          br,
+          content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }),
+          br,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.location + ':'), span({ class: 'card-value' }, content.location)) : null,
+          Array.isArray(content.members) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeMembersCount + ':'), span({ class: 'card-value' }, content.members.length)) : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'audio':
+        return content.url ? div({ class: 'search-audio' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
+          br,
+          audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' }),
+          br,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        ) : null;
+      case 'image':
+        return content.url ? div({ class: 'search-image' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : null,
+          br,
+          img({ src: `/blob/${encodeURIComponent(content.url)}` }),
+          br,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        ) : null;
+      case 'video':
+        return content.url ? div({ class: 'search-video' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
+          br,
+          videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360' }),
+          br,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        ) : null;
+      case 'document':
+        return div({ class: 'search-document' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          br,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
+          br,
+          div({
+            id: `pdf-container-${content.key || content.url}`,
+            class: 'pdf-viewer-container',
+            'data-pdf-url': `/blob/${encodeURIComponent(content.url)}`
+          }),
+          br,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'market':
+        return div({ class: 'search-market' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, content.item_type.toUpperCase())) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemCondition + ':'), span({ class: 'card-value' }, content.status)) : null,
+          content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemDeadline + ':'), span({ class: 'card-value' }, new Date(content.deadline).toLocaleString())) : null,
+          br,
+          content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}`, class: 'market-image' }) : null,
+          br,
+          content.seller ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemSeller + ':'), span({ class: 'card-value' }, a({ class: "user-link", href: `/author/${encodeURIComponent(content.seller)}` }, content.seller))) : null,
+          content.stock ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock || 'N/A')) : null,
+          content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriceLabel + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null,
+          content.condition ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.condition)) : null,
+          content.includesShipping ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemIncludesShipping + ':'), span({ class: 'card-value' }, `${content.includesShipping ? i18n.YESLabel : i18n.NOLabel}`)) : null,
+          content.auctions_poll && content.auctions_poll.length > 0
+            ? div({ class: 'auction-info' },
+                p(i18n.marketAuctionBids),
+                table({ class: 'auction-bid-table' },
+                  tr(
+                    th(i18n.marketAuctionBidTime),
+                    th(i18n.marketAuctionUser),
+                    th(i18n.marketAuctionBidAmount)
+                  ),
+                  content.auctions_poll.map(bid => {
+                    const [userId, bidAmount, bidTime] = bid.split(':');
+                    return tr(
+                      td(moment(bidTime).format('YYYY-MM-DD HH:mm:ss')),
+                      td(a({ href: `/author/${encodeURIComponent(userId)}` }, userId)),
+                      td(`${parseFloat(bidAmount).toFixed(6)} ECO`)
+                    );
+                  })
+                )
+            )
+            : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'bookmark':
+        return div({ class: 'search-bookmark' },
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.url ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.url, target: '_blank' }, content.url))) : null,
+          content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkCategory + ':'), span({ class: 'card-value' }, content.category)) : null,
+          content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())) : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'task':
+        return div({ class: 'search-task' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
+          content.priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriorityLabel + ':'), span({ class: 'card-value' }, content.priority)) : null,
+          typeof content.isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchIsPublicLabel + ':'), span({ class: 'card-value' }, content.isPublic ? i18n.YESLabel : i18n.NOLabel)) : null,
+          content.startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskStartTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.startTime).toLocaleString())) : null,
+          content.endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskEndTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.endTime).toLocaleString())) : null,
+          Array.isArray(content.assignees) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskAssignees + ':'), span({ class: 'card-value' }, content.assignees.length)) : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'report':
+        return div({ class: 'search-report' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
+          content.severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsSeverity + ':'), span({ class: 'card-value' }, content.severity)) : null,
+          content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
           br,
-          contentHtml,
-          author
-            ? p({ class: 'card-footer' },
-             span({ class: 'date-link' }, `${created} ${i18n.performed} `),
-             a({ href: authorUrl, class: 'user-link' }, `${author}`)
-          ): null, 
-          
-        ]);
-      })
+          content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}` }) : null,
+          br,
+          typeof content.confirmations === 'number' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsConfirmations + ':'), span({ class: 'card-value' }, content.confirmations)) : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'transfer':
+        return div({ class: 'search-transfer' },
+          content.concept ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConcept + ':'), span({ class: 'card-value' }, content.concept)) : null,
+          content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersDeadline + ':'), span({ class: 'card-value' }, content.deadline)) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersStatus + ':'), span({ class: 'card-value' }, content.status)) : null,
+          content.amount ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersAmount + ':'), span({ class: 'card-value' }, content.amount)) : null,
+          br,
+          content.from ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersFrom + ':'), span({ class: 'card-value' }, a({ class: "user-link", href: `/author/${encodeURIComponent(content.from)}` }, content.from))) : null,
+          content.to ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersTo + ':'), span({ class: 'card-value' }, a({ class: "user-link", href: `/author/${encodeURIComponent(content.to)}` }, content.to))) : null,
+          br,
+          content.confirmedBy && content.confirmedBy.length
+            ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConfirmations + ':'), span({ class: 'card-value' }, content.confirmedBy.length))
+            : null,
+          content.tags && content.tags.length
+            ? div({ class: 'card-tags' }, content.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+            ))
+            : null
+        );
+      case 'curriculum':
+        return div({ class: 'search-curriculum' },
+          content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvNameLabel + ':'), span({ class: 'card-value' }, content.name)) : null,
+          content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
+          content.photo ? img({ src: `/blob/${encodeURIComponent(content.photo)}`, class: 'curriculum-photo' }) : null,
+          content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
+          content.preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvPreferencesLabel + ':'), span({ class: 'card-value' }, content.preferences)) : null,
+          Array.isArray(content.personalSkills) && content.personalSkills.length
+            ? div({ class: 'card-field' }, content.personalSkills.map(skill =>
+                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: 'tag-link' }, `#${skill}`)
+              )) : null,
+          Array.isArray(content.personalExperiences) && content.personalExperiences.length
+            ? div({ class: 'card-field' }, content.personalExperiences.map(exp => p(exp))) : null,
+          Array.isArray(content.oasisExperiences) && content.oasisExperiences.length
+            ? div({ class: 'card-field' }, content.oasisExperiences.map(exp => p(exp))) : null,
+          Array.isArray(content.oasisSkills) && content.oasisSkills.length
+            ? div({ class: 'card-field' }, content.oasisSkills.map(skill => p(skill))) : null,
+          Array.isArray(content.educationExperiences) && content.educationExperiences.length
+            ? div({ class: 'card-field' }, content.educationExperiences.map(exp => p(exp))) : null,
+          Array.isArray(content.educationalSkills) && content.educationalSkills.length
+            ? div({ class: 'card-field' }, content.educationalSkills.map(skill =>
+                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: 'tag-link' }, `#${skill}`)
+              )) : null,
+          Array.isArray(content.languages) && content.languages.length
+            ? div({ class: 'card-field' }, content.languages.map(lang => p(lang))) : null,
+          Array.isArray(content.professionalExperiences) && content.professionalExperiences.length
+            ? div({ class: 'card-field' }, content.professionalExperiences.map(exp => p(exp))) : null,
+          Array.isArray(content.professionalSkills) && content.professionalSkills.length
+            ? div({ class: 'card-field' }, content.professionalSkills.map(skill => p(skill))) : null
+        );
+      case 'bankWallet':
+        return div({ class: 'search-bank-wallet' },
+          content.address ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.bankWalletConnected + ':' ),
+            span({ class: 'card-value' }, content.address)
+          ) : null
+        );
+      case 'bankClaim':
+        return div({ class: 'search-bank-claim' },
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.bankUbiReceived + ':' ),
+            span({ class: 'card-value' }, `${Number(content.amount || 0).toFixed(6)} ECO`)
+          ),
+          content.epochId ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.bankEpochShort + ':' ),
+            span({ class: 'card-value' }, content.epochId)
+          ) : null,
+          content.allocationId ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.bankAllocId + ':' ),
+            span({ class: 'card-value' }, content.allocationId)
+          ) : null,
+          content.txid ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.bankTx + ':' ),
+            a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${content.txid}`, target: '_blank' }, content.txid)
+          ) : null
+        );
+      case 'job':
+        return div({ class: 'search-job' },
+          content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
+          content.salary ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSalary + ':'), span({ class: 'card-value' }, `${content.salary} ECO`)) : null,
+          content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobStatus + ':'), span({ class: 'card-value' }, content.status.toUpperCase())) : null,
+          content.job_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobType + ':'), span({ class: 'card-value' }, content.job_type.toUpperCase())) : null,
+          content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobLocation + ':'), span({ class: 'card-value' }, String(content.location).toUpperCase())) : null,
+          content.vacants ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), span({ class: 'card-value' }, content.vacants)) : null,
+          Array.isArray(content.subscribers) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, `${content.subscribers.length}`)) : null
+        );
+      case 'forum':
+        return div({ class: 'search-forum' },
+          content.root ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title || '')) : div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title || '')),
+          content.text ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.text)) : null
+        );
+      case 'vote':
+        return div({ class: 'search-vote-link' },
+          content.vote && content.vote.link ? p(a({ href: `/thread/${encodeURIComponent(content.vote.link)}#${encodeURIComponent(content.vote.link)}`, class: 'activityVotePost' }, content.vote.link)) : null
+        );
+      case 'contact':
+        return div({ class: 'search-contact' },
+          content.contact ? p(a({ href: `/author/${encodeURIComponent(content.contact)}`, class: 'activitySpreadInhabitant2' }, content.contact)) : null
+        );
+      case 'pub':
+        return div({ class: 'search-pub' },
+          content.address && content.address.key ? p(a({ href: `/author/${encodeURIComponent(content.address.key)}`, class: 'activitySpreadInhabitant2' }, content.address.key)) : null
+        );
+      default:
+        return div({ class: 'styled-text', innerHTML: renderTextWithStyles(content.text || content.description || content.title || '[no content]') });
+    }
+  };
+
+  const resultSection = Object.entries(results).length > 0
+    ? Object.entries(results).map(([key, msgs]) =>
+      div(
+        { class: "search-result-group" },
+        h2(i18n[key + "Label"] || key),
+        ...msgs.map((msg) => {
+          const content = msg.value.content || {};
+          const created = new Date(msg.timestamp).toLocaleString();
+          if (content.type === 'document') hasDocument = true;
+          const contentHtml = renderContentHtml(content);
+          let author;
+          let authorUrl = '#';
+          if (content.type === 'market') {
+            author = content.seller || i18n.anonymous || "Anonymous";
+            authorUrl = `/author/${encodeURIComponent(content.seller)}`;
+          } else if (content.type === 'event') {
+            author = content.organizer || i18n.anonymous || "Anonymous";
+            authorUrl = `/author/${encodeURIComponent(content.organizer)}`;
+          } else if (content.type === 'transfer') {
+            author = content.from || i18n.anonymous || "Anonymous";
+            authorUrl = `/author/${encodeURIComponent(content.from)}`;
+          } else if (content.type === 'post' || content.type === 'about') {
+            author = msg.value.author || i18n.anonymous || "Anonymous";
+            authorUrl = `/author/${encodeURIComponent(msg.value.author)}`;
+          } else if (content.type === 'report') {
+            author = content.author || i18n.anonymous || "Anonymous";
+            authorUrl = `/author/${encodeURIComponent(content.author || 'Anonymous')}`;
+          } else if (content.type === 'votes') {
+            author = content.createdBy || i18n.anonymous || "Anonymous";
+            authorUrl = `/author/${encodeURIComponent(content.createdBy || 'Anonymous')}`;
+          } else {
+            author = content.author;
+            authorUrl = `/author/${encodeURIComponent(content.author || 'Anonymous')}`;
+          }
+
+          const contentId = msg.key;
+          const detailsButton = form({ method: "GET", action: getViewDetailsActionForSearch(content.type, contentId, content) },
+            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+          );
+
+          return div({ class: 'result-item' }, [
+            detailsButton,
+            br,
+            contentHtml,
+            author
+              ? p({ class: 'card-footer' },
+               span({ class: 'date-link' }, `${created} ${i18n.performed} `),
+               a({ href: authorUrl, class: 'user-link' }, `${author}`)
+            ): null,
+          ]);
+        })
+      )
     )
-  )
-  : div({ class: 'no-results' }, p(i18n.noResultsFound));
+    : div({ class: 'no-results' }, p(i18n.noResultsFound));
 
   let html = template(
     hashtag ? `#${hashtag}` : i18n.searchTitle,
@@ -439,4 +499,3 @@ const resultSection = Object.entries(results).length > 0
 };
 
 exports.searchView = searchView;
-

+ 23 - 14
src/views/stats_view.js

@@ -11,9 +11,11 @@ exports.statsView = (stats, filter) => {
   const types = [
     'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project',
     'image', 'audio', 'video', 'document', 'transfer', 'post', 'tribe',
-    'market', 'forum', 'job', 'aiExchange', 'karmaScore'
+    'market', 'forum', 'job', 'aiExchange'
   ];
-  const totalContent = types.reduce((sum, t) => sum + C(stats, t), 0);
+  const totalContent = types
+  .filter(t => t !== 'karmaScore') 
+  .reduce((sum, t) => sum + C(stats, t), 0);
   const totalOpinions = types.reduce((sum, t) => sum + O(stats, t), 0);
   const blockStyle = 'padding:16px;border:1px solid #ddd;border-radius:8px;margin-bottom:24px;';
   const headerStyle = 'background-color:#f8f9fa; padding:24px; border-radius:8px; border:1px solid #e0e0e0; box-shadow:0 2px 8px rgba(0,0,0,0.1);';
@@ -33,7 +35,6 @@ exports.statsView = (stats, filter) => {
           )
         )
       ),
-
       section(
         div({ style: headerStyle },
           h3({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsCreatedAt}: `, span({ style: 'color:#888;' }, stats.createdAt)),
@@ -56,6 +57,8 @@ exports.statsView = (stats, filter) => {
             )
           )
         ),
+        
+        div({ style: headerStyle }, h3(`${i18n.bankingUserEngagementScore}: ${C(stats, 'karmaScore')}`)),
 
         div({ style: headerStyle },
           h3({ style: 'font-size:18px; color:#555; margin:8px 0; font-weight:600;' }, i18n.statsBankingTitle),
@@ -85,11 +88,10 @@ exports.statsView = (stats, filter) => {
               span({ style: 'color:#888;' }, String(C(stats, 'aiExchange') || 0))
             )
           )
-        ),
+        ),     
 
         filter === 'ALL'
           ? div({ class: 'stats-container' }, [
-              div({ style: blockStyle }, h2(`${i18n.bankingUserEngagementScore}: ${C(stats, 'karmaScore')}`)),
               div({ style: blockStyle },
                 h2(i18n.statsActivity7d),
                 table({ style: 'width:100%; border-collapse: collapse;' },
@@ -155,14 +157,17 @@ exports.statsView = (stats, filter) => {
                 h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`),
                 ul(types.map(t => O(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${O(stats, t)}`) : null).filter(Boolean))
               ),
-              div({ style: blockStyle },
-                h2(`${i18n.statsNetworkContent}: ${totalContent}`),
-                ul(types.map(t => C(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`) : null).filter(Boolean))
-              )
+	      div({ style: blockStyle },
+		  h2(`${i18n.statsNetworkContent}: ${totalContent}`),
+		  ul(types
+		    .filter(t => t !== 'karmaScore')
+		    .map(t => C(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`) : null)
+		    .filter(Boolean)
+		  )
+	      )
             ])
           : filter === 'MINE'
             ? div({ class: 'stats-container' }, [
-                div({ style: blockStyle }, h2(`${i18n.bankingUserEngagementScore}: ${C(stats, 'karmaScore')}`)),
                 div({ style: blockStyle },
                   h2(i18n.statsActivity7d),
                   table({ style: 'width:100%; border-collapse: collapse;' },
@@ -215,10 +220,14 @@ exports.statsView = (stats, filter) => {
                   h2(`${i18n.statsYourOpinions}: ${totalOpinions}`),
                   ul(types.map(t => O(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${O(stats, t)}`) : null).filter(Boolean))
                 ),
-                div({ style: blockStyle },
-                  h2(`${i18n.statsYourContent}: ${totalContent}`),
-                  ul(types.map(t => C(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`) : null).filter(Boolean))
-                )
+		div({ style: blockStyle },
+		  h2(`${i18n.statsYourContent}: ${totalContent}`),
+		  ul(types
+		    .filter(t => t !== 'karmaScore') 
+		    .map(t => C(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`) : null)
+		    .filter(Boolean)
+		  )
+		)
               ])
             : div({ class: 'stats-container' }, [
                 div({ style: blockStyle },