Browse Source

Oasis release 0.3.7

psy 19 hours ago
parent
commit
aff4fc99cb
49 changed files with 3142 additions and 2690 deletions
  1. 41 75
      src/backend/backend.js
  2. 47 27
      src/client/assets/styles/style.css
  3. 149 0
      src/client/assets/themes/Clear-SNH.css
  4. 146 0
      src/client/assets/themes/Dark-SNH.css
  5. 149 0
      src/client/assets/themes/Matrix-SNH.css
  6. 149 0
      src/client/assets/themes/Purple-SNH.css
  7. 4 2
      src/client/assets/translations/oasis_en.js
  8. 6 4
      src/client/assets/translations/oasis_es.js
  9. 4 2
      src/client/assets/translations/oasis_eu.js
  10. 115 0
      src/client/public/docs/ecoin.md
  11. 73 125
      src/models/events_model.js
  12. 28 29
      src/models/main_models.js
  13. 191 133
      src/models/market_model.js
  14. 10 6
      src/models/opinions_model.js
  15. 1 6
      src/models/privatemessages_model.js
  16. 89 179
      src/models/reports_model.js
  17. 6 3
      src/models/search_model.js
  18. 14 12
      src/models/tags_model.js
  19. 66 174
      src/models/tasks_model.js
  20. 16 8
      src/models/trending_model.js
  21. 60 236
      src/models/votes_model.js
  22. 1 1
      src/server/package-lock.json
  23. 1 1
      src/server/package.json
  24. 314 415
      src/views/activity_view.js
  25. 149 118
      src/views/agenda_view.js
  26. 50 33
      src/views/audio_view.js
  27. 83 50
      src/views/bookmark_view.js
  28. 3 3
      src/views/cv_view.js
  29. 29 24
      src/views/document_view.js
  30. 110 88
      src/views/event_view.js
  31. 5 3
      src/views/feed_view.js
  32. 38 29
      src/views/image_view.js
  33. 3 3
      src/views/inhabitants_view.js
  34. 14 19
      src/views/main_views.js
  35. 120 92
      src/views/market_view.js
  36. 194 186
      src/views/opinions_view.js
  37. 0 1
      src/views/peers_view.js
  38. 1 1
      src/views/pixelia_view.js
  39. 83 72
      src/views/report_view.js
  40. 196 139
      src/views/search_view.js
  41. 4 1
      src/views/settings_view.js
  42. 2 2
      src/views/stats_view.js
  43. 1 3
      src/views/tags_view.js
  44. 62 102
      src/views/task_view.js
  45. 98 37
      src/views/transfer_view.js
  46. 123 177
      src/views/trending_view.js
  47. 16 13
      src/views/tribes_view.js
  48. 39 29
      src/views/video_view.js
  49. 39 27
      src/views/vote_view.js

+ 41 - 75
src/backend/backend.js

@@ -1122,10 +1122,6 @@ router
     }
     const eventId = ctx.params.id;
     const event = await eventsModel.getEventById(eventId);
-    if (event.opinions_inhabitants && event.opinions_inhabitants.length > 0) {
-        ctx.flash = { message: "This event has received votes and cannot be updated." };
-        ctx.redirect(`/events?filter=mine`);
-    }
     ctx.body = await eventView([event], 'edit', eventId);
    })
   .get('/events/:eventId', async ctx => {
@@ -1695,14 +1691,14 @@ router
   })
   .post('/documents/create', koaBody({ multipart: true }), async (ctx) => {
     const docBlob = await handleBlobUpload(ctx, 'document');
-    const { tags } = ctx.request.body;
-    await documentsModel.createDocument(docBlob, tags);
+    const { tags, title, description } = ctx.request.body;
+    await documentsModel.createDocument(docBlob, tags, title, description);
     ctx.redirect('/documents');
   })
   .post('/documents/update/:id', koaBody({ multipart: true }), async (ctx) => {
-    const { tags } = ctx.request.body;
+    const { tags, title, description } = ctx.request.body;
     const blob = ctx.request.files?.document ? await handleBlobUpload(ctx, 'document') : null;
-    await documentsModel.updateDocumentById(ctx.params.id, blob, tags);
+    await documentsModel.updateDocumentById(ctx.params.id, blob, tags, title, description);
     ctx.redirect('/documents?filter=mine');
   })
   .post('/documents/delete/:id', koaBody(), async (ctx) => {
@@ -1882,11 +1878,6 @@ router
     const { title, description, startTime, endTime, priority, location, tags, isPublic } = ctx.request.body;
     const taskId = ctx.params.id;
     const task = await tasksModel.getTaskById(taskId);
-    if (task.opinions_inhabitants && task.opinions_inhabitants.length > 0) {
-      ctx.flash = { message: "This task has received votes and cannot be updated." };
-      ctx.redirect(`/tasks?filter=mine`);
-      return;
-    }
     await tasksModel.updateTaskById(taskId, {
       title,
       description,
@@ -1900,46 +1891,34 @@ router
       author: task.author
     });
     ctx.redirect('/tasks?filter=mine');
-  })
+   })
   .post('/tasks/assign/:id', koaBody(), async (ctx) => {
     const taskId = ctx.params.id;
     await tasksModel.toggleAssignee(taskId);
     ctx.redirect('/tasks');
-  })
+   })
   .post('/tasks/delete/:id', koaBody(), async (ctx) => {
     const taskId = ctx.params.id;
     await tasksModel.deleteTaskById(taskId);
     ctx.redirect('/tasks?filter=mine');
-  })
+   })
   .post('/tasks/status/:id', koaBody(), async (ctx) => {
     const taskId = ctx.params.id;
     const { status } = ctx.request.body;
     await tasksModel.updateTaskStatus(taskId, status);
     ctx.redirect('/tasks?filter=mine');
-  })
-  .post('/tasks/opinions/:taskId/:category', async (ctx) => {
-    const { taskId, category } = ctx.params;
-    const voterId = config?.keys?.id;
-    const task = await tasksModel.getTaskById(taskId);
-    if (task.opinions_inhabitants && task.opinions_inhabitants.includes(voterId)) {
-      ctx.flash = { message: "You have already opined." };
-      ctx.redirect('/tasks');
-      return;
-    }
-    await opinionsModel.createVote(taskId, category, 'task');
-    ctx.redirect('/tasks');
    })
-   .post('/reports/create', koaBody({ multipart: true }), async ctx => {
-      const { title, description, category, tags, severity, isAnonymous } = ctx.request.body;
+  .post('/reports/create', koaBody({ multipart: true }), async ctx => {
+      const { title, description, category, tags, severity } = ctx.request.body;
       const image = await handleBlobUpload(ctx, 'image');
-      await reportsModel.createReport(title, description, category, image, tags, severity, !!isAnonymous);
+      await reportsModel.createReport(title, description, category, image, tags, severity);
       ctx.redirect('/reports');
-    })
-   .post('/reports/update/:id', koaBody({ multipart: true }), async ctx => {
-      const { title, description, category, tags, severity, isAnonymous } = ctx.request.body;
+   })
+  .post('/reports/update/:id', koaBody({ multipart: true }), async ctx => {
+      const { title, description, category, tags, severity } = ctx.request.body;
       const image = await handleBlobUpload(ctx, 'image');
       await reportsModel.updateReportById(ctx.params.id, {
-        title, description, category, image, tags, severity, isAnonymous: !!isAnonymous
+        title, description, category, image, tags, severity
       });
       ctx.redirect('/reports?filter=mine');
    })
@@ -1947,18 +1926,6 @@ router
       await reportsModel.deleteReportById(ctx.params.id);
       ctx.redirect('/reports?filter=mine');
    })   
-  .post('/reports/opinions/:reportId/:category', async (ctx) => {
-    const { reportId, category } = ctx.params;
-    const voterId = SSBconfig?.keys?.id;
-    const report = await reportsModel.getReportById(reportId);
-    if (report.opinions_inhabitants && report.opinions_inhabitants.includes(voterId)) {
-      ctx.flash = { message: "You have already opined." };
-      ctx.redirect('/reports');
-      return;
-    }
-    await opinionsModel.createVote(reportId, category, 'report');
-    ctx.redirect('/reports');
-   })
   .post('/reports/confirm/:id', async ctx => {
       await reportsModel.confirmReportById(ctx.params.id);
       ctx.redirect('/reports');
@@ -1978,10 +1945,6 @@ router
     const { title, description, date, location, price, url, attendees, tags, isPublic } = ctx.request.body;
     const eventId = ctx.params.id;
     const event = await eventsModel.getEventById(eventId);
-    if (event.opinions_inhabitants && event.opinions_inhabitants.length > 0) {
-        ctx.flash = { message: "This event has received votes and cannot be updated." };
-        ctx.redirect(`/events?filter=mine`);
-    }
     await eventsModel.updateEventById(eventId, {
       title,
       description,
@@ -2007,18 +1970,6 @@ router
     await eventsModel.deleteEventById(eventId);
     ctx.redirect('/events?filter=mine');
   })
-  .post('/events/opinions/:eventId/:category', async (ctx) => {
-    const { eventId, category } = ctx.params;
-    const voterId = SSBconfig?.keys?.id;
-    const event = await eventsModel.getEventById(eventId);
-    if (event.opinions_inhabitants && event.opinions_inhabitants.includes(voterId)) {
-      ctx.flash = { message: "You have already opined." };
-      ctx.redirect('/events');
-      return;
-    }
-    await opinionsModel.createVote(eventId, category, 'event');
-    ctx.redirect('/events');
-  })
   .post('/votes/create', koaBody(), async ctx => {
     const { question, deadline, options = 'YES,NO,ABSTENTION', tags = '' } = ctx.request.body;
     const parsedOptions = options.split(',').map(o => o.trim()).filter(Boolean);
@@ -2058,15 +2009,21 @@ router
     ctx.redirect('/votes');
   })
   .post('/market/create', koaBody({ multipart: true }), async ctx => {
-    const { item_type, title, description, price, tags, item_status, deadline, includesShipping } = ctx.request.body;
+    const { item_type, title, description, price, tags, item_status, deadline, includesShipping, stock } = ctx.request.body;
     const image = await handleBlobUpload(ctx, 'image');
-    await marketModel.createItem(item_type, title, description, image, price, tags, item_status, deadline, includesShipping);
-    ctx.redirect('/market');
+    if (!stock || stock <= 0) {
+      ctx.throw(400, 'Stock must be a positive number.');
+    }
+    await marketModel.createItem(item_type, title, description, image, price, tags, item_status, deadline, includesShipping, stock);
+   ctx.redirect('/market');
   })
   .post('/market/update/:id', koaBody({ multipart: true }), async ctx => {
     const id = ctx.params.id;
-    const { item_type, title, description, price, tags = '', item_status, deadline, includesShipping } = ctx.request.body;
+    const { item_type, title, description, price, tags = '', item_status, deadline, includesShipping, stock } = ctx.request.body;
     const parsedTags = tags.split(',').map(t => t.trim()).filter(Boolean);
+    if (stock < 0) {
+      ctx.throw(400, 'Stock cannot be negative.');
+    }
     const updatedData = {
       item_type, 
       title, 
@@ -2075,7 +2032,8 @@ router
       item_status, 
       deadline, 
       includesShipping, 
-      tags: parsedTags  
+      tags: parsedTags, 
+      stock
     };
     const image = await handleBlobUpload(ctx, 'image');
     updatedData.image = image;
@@ -2090,8 +2048,12 @@ router
   .post('/market/sold/:id', koaBody(), async ctx => {
     const id = ctx.params.id;
     const marketItem = await marketModel.getItemById(id);
+    if (marketItem.stock <= 0) {
+      ctx.throw(400, 'No stock left to mark as sold.');
+    }
     if (marketItem.status !== 'SOLD') {
       await marketModel.setItemAsSold(id);
+      await marketModel.decrementStock(id);
     }
     ctx.redirect('/market?filter=mine');
   })
@@ -2100,21 +2062,25 @@ router
     const marketItem = await marketModel.getItemById(id);
     if (marketItem.item_type === 'exchange') {
       if (marketItem.status !== 'SOLD') {
-      const buyerId = ctx.request.body.buyerId;
-      const { price, title, seller } = marketItem;
-      const subject = `Your item "${title}" has been sold`;
-      const text = `The item with title: "${title}" has been sold. The buyer with OASIS ID: ${buyerId} purchased it for: $${price}.`;
-      await pmModel.sendMessage([seller], subject, text);
-      await marketModel.setItemAsSold(id);
+        const buyerId = ctx.request.body.buyerId;
+        const { price, title, seller } = marketItem;
+        const subject = `Your item "${title}" has been sold`;
+        const text = `The item with title: "${title}" has been sold. The buyer with OASIS ID: ${buyerId} purchased it for: $${price}.`;
+        await pmModel.sendMessage([seller], subject, text);
+        await marketModel.setItemAsSold(id);
       }
     }
+    await marketModel.decrementStock(id);
     ctx.redirect('/inbox?filter=sent');
   })
- .post('/market/bid/:id', koaBody(), async ctx => {
+  .post('/market/bid/:id', koaBody(), async ctx => {
     const id = ctx.params.id;
     const { bidAmount } = ctx.request.body;
     const marketItem = await marketModel.getItemById(id);
     await marketModel.addBidToAuction(id, userId, bidAmount);
+    if (marketItem.stock > 0 && marketItem.status === 'SOLD') {
+      await marketModel.decrementStock(id);
+    }
     ctx.redirect('/market?filter=auctions');
   })
 

+ 47 - 27
src/client/assets/styles/style.css

@@ -793,7 +793,7 @@ button.create-button:hover {
 
 .inhabitant-photo {
   width: 64px;
-  height: 64px;
+  height: 64px !important;;
   object-fit: cover;
   border-radius: 50%;
   margin-right: 1rem;
@@ -1474,52 +1474,73 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
 }
 
 /* Market */
-.market-item {
-  display: flex;
-  margin-bottom: 20px;
+.market-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
+  justify-content: center;
 }
 
 .market-card {
+  background-color: #2c2c2c;
+  border-radius: 16px;
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
   display: flex;
-  flex-direction: row;
+  flex-direction: column;
+  justify-content: space-between;
+  text-align: center;
 }
 
-.market-card.left-col {
-  flex: 35%;
-  border: 0;
-  flex-direction: column;
+.market-card-image {
+  width: 100%;
+  max-height: 150px; 
+  object-fit: contain;
+  border-radius: 8px;
+  margin-bottom: 15px;
 }
 
-.market-card.right-col {
-  flex: 65%; 
-  border: 0;
-  flex-direction: column;
+.market-card-title {
+  font-size: 1.3em;
+  font-weight: bold;
+  color: #ffd600;
+  margin-bottom: 10px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis; 
 }
 
-.market-card.image {
-  max-width: 600px;
-  max-height: 500px;
-  border:0px;
+.market-card-description {
+  font-size: 0.9em;
+  color: #ccc;
+  margin-bottom: 10px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap; 
 }
 
-.market-card.right-col .market-card.buttons {
+.market-card-footer {
+  margin-top: auto;
   display: flex;
-  gap:10px;
-  border:0;
+  flex-direction: column;
+  gap: 8px;
+  font-size: 1.1em;
+  color: #ffd600;
 }
 
-.market-card.right-col .market-card.buttons form {
-  width: 100%;
+.market-card-footer .condition,
+.market-card-footer .stock {
+  font-size: 1em;
+  color: #ff9900;
 }
 
-.market-card.price {
+.market-card-footer .stock {
   font-weight: bold;
-  color: #ff6600;
+  color: #ff0000;
 }
 
-.auction-bid-text {
+.market-card.price {
+  font-size: 1.3em;
   font-weight: bold;
-  color: #ff6600;
 }
 
 /*avatar relationships*/
@@ -1575,4 +1596,3 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
   border-color: #FF6A00;
   font-weight: bold;
 }
-

+ 149 - 0
src/client/assets/themes/Clear-SNH.css

@@ -212,3 +212,152 @@ footer a:hover {
   background-color: #FF8F00 !important;
   color: #FFFFFF !important;
 }
+
+.card {
+  border-radius: 16px;
+  padding: 16px 24px;
+  margin-bottom: 24px;
+  color: #4A4A4A;
+  font-family: inherit;
+  box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.1);
+  background-color: #F4F4F4;
+}
+
+.card-section {
+  border: none;
+  padding: 16px 0 0 16px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 10px;
+  margin-top: 16px;
+  padding-top: 0px;
+  border: none;
+}
+
+.card-label {
+  color: #2D2D2D;
+  font-weight: bold;
+  letter-spacing: 1.5px;
+  line-height: 1.2;
+  margin-bottom: 8px;
+}
+
+.card-footer {
+  margin-top: 12px;
+  font-weight: 500;
+  color: #6C6C6C;
+  font-size: 1.1em;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  background: none;
+  border: none;
+  padding-top: 0;
+  margin-bottom: 12px;
+}
+
+.card-body {
+  margin-top: 12px;
+  margin-bottom: 16px;
+  padding: 0;
+}
+
+.card-field {
+  display: flex;
+  align-items: baseline;
+  padding: 0;
+  margin-bottom: 8px;
+  border: none;
+  background: none;
+}
+
+.card-tags {
+  margin: 5px 0 3px 0;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 9px;
+}
+
+.card-tags a.tag-link {
+  text-decoration: none;
+  color: #181818;
+  background: #D94F4F;
+  padding: 5px 13px 4px 13px;
+  border-radius: 7px;
+  font-size: .98em;
+  border: none;
+  font-weight: bold;
+}
+
+.card-tags a.tag-link:hover {
+  background: #D94F4F;
+  color: #111;
+  cursor: pointer;
+}
+
+a.user-link {
+  background-color: #FFD600;
+  color: #FFFFFF;
+  padding: 12px 24px;
+  border-radius: 5px;
+  text-align: center;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid #FFD600;
+  transition: background-color 0.3s, color 0.3s, border-color 0.3s;
+  font-size: 1em;
+}
+
+a.user-link:hover {
+  background-color: #FFD600;
+  border-color: #FFD600;
+  color: #FFFFFF;
+  cursor: pointer;
+}
+
+a.user-link:focus {
+  background-color: #9A2F2F;
+  border-color: #9A2F2F;
+  color: #FFFFFF;
+}
+
+.date-link {
+  background-color: #2F3C32;
+  color: #fff;
+  padding: 12px 24px;
+  border-radius: 8px;
+  margin-left: 12px;
+}
+
+.date-link:hover {
+  background-color: #3E4A3D;
+  color: #fff;
+}
+
+.activitySpreadInhabitant2 {
+  background-color: #3E4A3D;
+  color: #fff;
+  padding: 12px 24px;
+  border-radius: 8px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}
+
+.activityVotePost {
+  background-color: #3B5C42;
+  color: #fff;
+  padding: 12px 24px;
+  border-radius: 8px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}
+

+ 146 - 0
src/client/assets/themes/Dark-SNH.css

@@ -142,3 +142,149 @@ footer a:hover {
   color: #FFDD44;
 }
 
+.card {
+  border-radius: 16px;
+  padding: 0px 24px 10px 24px; 
+  margin-bottom: 16px;
+  color: #FFD600;
+  font-family: inherit;
+  box-shadow: 0 2px 20px 0 #FFD60024;
+}
+
+.card-section {
+  border:none;
+  padding: 10px 0 0 16px; 
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 0px; 
+  margin-top: 8px;
+  padding-top: 0px;   
+  border: none;
+}
+
+.card-label {
+  color: #ffa300;
+  font-weight: bold;
+  letter-spacing: 1.5px;
+  line-height: 1.2;
+  margin-bottom: 0;
+}
+
+.card-footer {
+  margin-top: 6px;  
+  font-weight: 500;
+  color: #ff9900;
+  font-size: 1.07em;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  background: none;
+  border: none;
+  padding-top: 0;
+  margin-bottom: 6;
+}
+
+.card-body {
+  margin-top: 0; 
+  margin-bottom: 4;
+  padding: 0; 
+}
+
+.card-field {
+  display: flex;
+  align-items: baseline;
+  padding: 0;
+  margin-bottom: 0;
+  border: none;
+  background: none;
+}
+
+.card-tags {
+  margin: 5px 0 3px 0;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 9px;
+}
+
+.card-tags a.tag-link {
+  text-decoration: none;
+  color: #181818;
+  background: #FFD600;
+  padding: 5px 13px 4px 13px;
+  border-radius: 7px;
+  font-size: .98em;
+  border: none;
+  font-weight: bold;
+}
+
+.card-tags a.tag-link:hover {
+  background: #ffe86a;
+  color: #111;
+  cursor: pointer;
+}
+
+a.user-link {
+  background-color: #FFA500; 
+  color: #000;
+  padding: 8px 16px;
+  border-radius: 5px; 
+  text-align: center;
+  font-weight: bold;
+  text-decoration: none; 
+  display: inline-block;
+  border: 2px solid transparent;
+  transition: background-color 0.3s, color 0.3s, border-color 0.3s; 
+  font-size: 0.8em; 
+}
+
+a.user-link:hover {
+  background-color: #FFD700; 
+  border-color: #FFD700; 
+  color: #000; 
+  cursor: pointer; 
+}
+
+a.user-link:focus {
+  background-color: #007B9F; 
+  border-color: #007B9F; 
+  color: #fff;
+}
+
+.date-link {
+  background-color: #444;
+  color: #FFD600;
+  padding: 8px 16px;
+  border-radius: 5px;
+  margin-left: 8px;
+}
+
+.date-link:hover {
+  background-color: #555;
+  color: #FFD700;
+}
+
+.activitySpreadInhabitant2 {
+  background-color: #007B9F; 
+  color: #fff;  
+  padding: 8px 16px;
+  border-radius: 5px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}
+
+.activityVotePost {
+  background-color: #557d3b; 
+  color: #fff;  
+  padding: 8px 16px;
+  border-radius: 5px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}

+ 149 - 0
src/client/assets/themes/Matrix-SNH.css

@@ -241,3 +241,152 @@ footer a {
   background-color: #00FF00 !important;
   color: #000000 !important;
 }
+
+.card {
+  border-radius: 16px;
+  padding: 16px 24px;
+  margin-bottom: 24px;
+  color: #00FF00;
+  font-family: 'Courier New', monospace;
+  box-shadow: 0 4px 30px 0 rgba(0, 255, 0, 0.2);
+  background-color: #1A1A1A;
+  border: 1px solid #00FF00;
+}
+
+.card-section {
+  border: none;
+  padding: 16px 0 0 16px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 10px;
+  margin-top: 16px;
+  padding-top: 0px;
+  border: none;
+}
+
+.card-label {
+  color: #00FF00;
+  font-weight: bold;
+  letter-spacing: 1.5px;
+  line-height: 1.2;
+  margin-bottom: 8px;
+}
+
+.card-footer {
+  margin-top: 12px;
+  font-weight: 500;
+  color: #00FF00;
+  font-size: 1.1em;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  background: none;
+  border: none;
+  padding-top: 0;
+  margin-bottom: 12px;
+}
+
+.card-body {
+  margin-top: 12px;
+  margin-bottom: 16px;
+  padding: 0;
+}
+
+.card-field {
+  display: flex;
+  align-items: baseline;
+  padding: 0;
+  margin-bottom: 8px;
+  border: none;
+  background: none;
+}
+
+.card-tags {
+  margin: 5px 0 3px 0;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 9px;
+}
+
+.card-tags a.tag-link {
+  text-decoration: none;
+  color: #00FF00;
+  background: #000000;
+  padding: 5px 13px 4px 13px;
+  border-radius: 7px;
+  font-size: .98em;
+  border: none;
+  font-weight: bold;
+}
+
+.card-tags a.tag-link:hover {
+  background: #00FF00;
+  color: #000000;
+  cursor: pointer;
+}
+
+a.user-link {
+  background-color: #00FF00;
+  color: #000000;
+  padding: 12px 24px;
+  border-radius: 5px;
+  text-align: center;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid #00FF00;
+  transition: background-color 0.3s, color 0.3s, border-color 0.3s;
+  font-size: 1em;
+}
+
+a.user-link:hover {
+  background-color: #000000;
+  border-color: #00FF00;
+  color: #00FF00;
+  cursor: pointer;
+}
+
+a.user-link:focus {
+  background-color: #00FF00;
+  border-color: #00FF00;
+  color: #000000;
+}
+
+.date-link {
+  background-color: #00FF00;
+  color: #000000;
+  padding: 12px 24px;
+  border-radius: 8px;
+  margin-left: 12px;
+}
+
+.date-link:hover {
+  background-color: #000000;
+  color: #00FF00;
+}
+
+.activitySpreadInhabitant2 {
+  background-color: #1A1A1A;
+  color: #00FF00;
+  padding: 12px 24px;
+  border-radius: 8px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}
+
+.activityVotePost {
+  background-color: #00FF00;
+  color: #000000;
+  padding: 12px 24px;
+  border-radius: 8px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}

+ 149 - 0
src/client/assets/themes/Purple-SNH.css

@@ -276,3 +276,152 @@ header {
 .sidebar-right nav ul li {
   width: 100%;
 }
+
+.card {
+  border-radius: 16px;
+  padding: 16px 24px;
+  margin-bottom: 24px;
+  color: #E5E5E5;
+  font-family: inherit;
+  box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.2);
+  background-color: #3C1360;
+  border: 1px solid #B86ADE;
+}
+
+.card-section {
+  border: none;
+  padding: 16px 0 0 16px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 10px;
+  margin-top: 16px;
+  padding-top: 0px;
+  border: none;
+}
+
+.card-label {
+  color: #9B1C96;
+  font-weight: bold;
+  letter-spacing: 1.5px;
+  line-height: 1.2;
+  margin-bottom: 8px;
+}
+
+.card-footer {
+  margin-top: 12px;
+  font-weight: 500;
+  color: #B86ADE;
+  font-size: 1.1em;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  background: none;
+  border: none;
+  padding-top: 0;
+  margin-bottom: 12px;
+}
+
+.card-body {
+  margin-top: 12px;
+  margin-bottom: 16px;
+  padding: 0;
+}
+
+.card-field {
+  display: flex;
+  align-items: baseline;
+  padding: 0;
+  margin-bottom: 8px;
+  border: none;
+  background: none;
+}
+
+.card-tags {
+  margin: 5px 0 3px 0;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 9px;
+}
+
+.card-tags a.tag-link {
+  text-decoration: none;
+  color: #FFFFFF;
+  background: #D94F4F;
+  padding: 5px 13px 4px 13px;
+  border-radius: 7px;
+  font-size: .98em;
+  border: none;
+  font-weight: bold;
+}
+
+.card-tags a.tag-link:hover {
+  background: #B86ADE;
+  color: #E5E5E5;
+  cursor: pointer;
+}
+
+a.user-link {
+  background-color: #FFD600;
+  color: #3C1360;
+  padding: 12px 24px;
+  border-radius: 5px;
+  text-align: center;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid #FFD600;
+  transition: background-color 0.3s, color 0.3s, border-color 0.3s;
+  font-size: 1em;
+}
+
+a.user-link:hover {
+  background-color: #FFB600;
+  border-color: #FFB600;
+  color: #3C1360;
+  cursor: pointer;
+}
+
+a.user-link:focus {
+  background-color: #9A2F2F;
+  border-color: #9A2F2F;
+  color: #E5E5E5;
+}
+
+.date-link {
+  background-color: #2F3C32;
+  color: #fff;
+  padding: 12px 24px;
+  border-radius: 8px;
+  margin-left: 12px;
+}
+
+.date-link:hover {
+  background-color: #3E4A3D;
+  color: #fff;
+}
+
+.activitySpreadInhabitant2 {
+  background-color: #3E4A3D;
+  color: #fff;
+  padding: 12px 24px;
+  border-radius: 8px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}
+
+.activityVotePost {
+  background-color: #3B5C42;
+  color: #fff;
+  padding: 12px 24px;
+  border-radius: 8px;
+  font-weight: bold;
+  text-decoration: none;
+  display: inline-block;
+  border: 2px solid transparent;
+}

+ 4 - 2
src/client/assets/translations/oasis_en.js

@@ -390,6 +390,7 @@ module.exports = {
     walletSentToLine: ({ destination, amount }) => `Sent ECO ${amount} to ${destination}`,
     walletSettingsTitle: "Wallet",
     walletSettingsDescription: "Integrate Oasis with your ECOin wallet.",
+    walletSettingsDocLink: "ECOin installation guide",
     walletStatusMessages: {
       invalid_amount: "Invalid amount",
       invalid_dest: "Invalid destination address",
@@ -583,6 +584,7 @@ module.exports = {
     skillsLabel:        "Skills",
     commonSkills:       "Common Skills",
     mutualFollowers:    "Mutual Followers",
+    latestInteractions: "Latest Interactions",
     viewAvatar:        "View Avatar",
     viewCV:            "View CV",
     suggestedSectionTitle: "Suggested",
@@ -693,8 +695,6 @@ module.exports = {
     taskArchivedTitle: "Archived Tasks",
     taskPublicTitle: "Public Tasks",
     taskPrivateTitle: "Private Tasks",
-    taskStartTimeLabel:   "Start Time",
-    taskEndTimeLabel:     "End Time",
     notasks: "No tasks available.",
     noLocation: "No location specified",
     //events
@@ -1330,6 +1330,8 @@ module.exports = {
     marketItemIncludesShipping: "Includes Shipping?",
     marketItemHighestBid: "Highest Bid",
     marketItemHighestBidder: "Highest Bidder",
+    marketItemStock: "Stock",
+    marketOutOfStock: "Out of stock",
     marketItemBidTime: "Bid Time",
     marketActionsUpdate: "Update",
     marketUpdateButton: "Update Item!",

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

@@ -389,6 +389,7 @@ module.exports = {
     walletSentToLine: ({ destination, amount }) => `Enviar ECO ${amount} a ${destination}`,
     walletSettingsTitle: "Cartera",
     walletSettingsDescription: "Integrar Oasis con tu cartera de ECOin.",
+    walletSettingsDocLink: "Guía de instalación de ECOin",
     walletStatusMessages: {
       invalid_amount: "Cantidad inválida",
       invalid_dest: "Dirección de destino inválida",
@@ -582,6 +583,7 @@ module.exports = {
     skillsLabel:        "Habilidades",
     commonSkills:       "Habilidades Comunes",
     mutualFollowers:    "Seguidores Mutuos",
+    latestInteractions: "Últimas Interacciones",
     viewAvatar:        "Ver Avatar",
     viewCV:            "Ver CV",
     suggestedSectionTitle: "Sugeridos",
@@ -644,8 +646,8 @@ module.exports = {
     tasksDescription: "Descubre y gestiona tareas en tu red.",
     taskTitleLabel: "Título",
     taskDescriptionLabel: "Descripción",
-    taskStartTimeLabel: "Hora de Inicio",
-    taskEndTimeLabel: "Hora de Fin",
+    taskStartTimeLabel: "Fecha de Inicio",
+    taskEndTimeLabel: "Fecha de Fin",
     taskPriorityLabel: "Prioridad",
     taskPrioritySelect: "Seleccionar prioridad",
     taskPriorityUrgent: "Urgente",
@@ -692,8 +694,6 @@ module.exports = {
     taskArchivedTitle: "Tareas Archivadas",
     taskPublicTitle: "Tareas Públicas",
     taskPrivateTitle: "Tareas Privadas",
-    taskStartTimeLabel:   "Hora de Inicio",
-    taskEndTimeLabel:     "Hora de Fin",
     notasks: "No hay tareas disponibles.",
     noLocation: "No se especificó ubicación",
     //events
@@ -1329,6 +1329,8 @@ module.exports = {
     marketItemIncludesShipping: "¿Incluye Envío?",
     marketItemHighestBid: "Oferta Más Alta",
     marketItemHighestBidder: "Mayor Postor",
+    marketItemStock: "Existencias",
+    marketOutOfStock: "Sin existencias",
     marketItemBidTime: "Tiempo de Oferta",
     marketActionsUpdate: "Actualizar",
     marketUpdateButton: "¡Actualizar Artículo!",

+ 4 - 2
src/client/assets/translations/oasis_eu.js

@@ -390,6 +390,7 @@ module.exports = {
     walletSentToLine: ({ destination, amount }) => `${amount} ECO bidali dira ${destination} helbidera`,
     walletSettingsTitle: "Zorroa",
     walletSettingsDescription: "Integratu Oasis zure ECOin zorroarekin.",
+    walletSettingsDocLink: "ECOin instalazio gida",
     walletStatusMessages: {
       invalid_amount: "Zenbatekoa oker",
       invalid_dest: "Hartzailearen helbidea oker",
@@ -583,6 +584,7 @@ module.exports = {
     skillsLabel:        "Trebetasunak",
     commonSkills:       "Trebetasunak amankomunean",
     mutualFollowers:    "Jarraitzaile amankomunean",
+    latestInteractions: "Interakzioak Azkenak",
     viewAvatar:        "Ikusi Abatarra",
     viewCV:            "Ikusi CV-a",
     suggestedSectionTitle: "Gomendatuta",
@@ -693,8 +695,6 @@ module.exports = {
     taskArchivedTitle: "Atazak artxibatuta",
     taskPublicTitle: "Ataza publikoak",
     taskPrivateTitle: "Ataza pribatuak",
-    taskStartTimeLabel:   "Hasiera Data",
-    taskEndTimeLabel:     "Amaiera Data",
     notasks: "Atazarik ez.",
     noLocation: "Ez da kokapenik zehaztu",
     //events
@@ -1330,6 +1330,8 @@ module.exports = {
     marketItemIncludesShipping: "Bidalketa barne?",
     marketItemHighestBid: "Eskaintza altuena",
     marketItemHighestBidder: "Gehien eskaintzen duena",
+    marketItemStock: "Stock",
+    marketOutOfStock: "Stock gabe",
     marketItemBidTime: "Eskantzaren data",
     marketActionsUpdate: "Eguneratu",
     marketUpdateButton: "Eguneratu elementua!",

+ 115 - 0
src/client/public/docs/ecoin.md

@@ -0,0 +1,115 @@
+
+ECOin - Copyright (c) - 2014/2025 - GPLv3 - epsylon@riseup.net (https://ecoin.03c8.net)
+
+===========================================
+# SOURCES for P2P Crypto-Currency (ECOin) #
+===========================================
+
+Testing machine is: Debian GNU/Linux 12 (bookworm) (x86_64).
+
+All of the commands should be executed in a shell.
+
+------------------------------
+
+(0.) Clone the github tree to get the source code:
+
+  + Official:
+
+	git clone http://code.03c8.net/epsylon/ecoin
+
+  + Mirror:
+
+	git clone https://github.com/epsylon/ecoin
+
+------------------------------
+
+===================================================
+# SERVER -ecoind- for P2P Crypto-Currency (ECOin) #
+===================================================
+
+(0.) Version libraries:
+
+    - Libboost -> source code 1.68 provided at: src/boost_1_68_0
+
+(1.) Install dependencies:
+
+    sudo apt-get install build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev libleveldb-dev miniupnpc libminiupnpc-dev
+
+        + Optionally install qrencode (and set USE_QRCODE=1):
+
+       	    sudo apt-get install libqrencode-dev
+
+(2.) Now you should be able to build ecoind:
+
+    cd src/
+    make -f makefile.linux USE_UPNP=- USE_IPV6=-
+    strip ecoind
+
+    An executable named 'ecoind' will be built.
+    
+    Now you can launch: ./ecoind to run your ECOin server.
+
+------------------------------
+
+============================================
+# WALLET for P2P Crypto-Currency (ECOin)  #
+============================================
+
+(0.) Version libraries:
+
+    - Libboost -> source code 1.68 provided at: src/boost_1_68_0
+
+(1.) First, make sure that the required packages for Qt5 development (an the others required for building the daemon) are installed:
+
+    sudo apt-get install qt5-qmake qtbase5-dev build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev libleveldb-dev miniupnpc libminiupnpc-dev
+        
+        + Optionally install qrencode (and set USE_QRCODE=1):
+
+       	    sudo apt-get install libqrencode-dev
+
+(2.) Then execute the following:
+
+    qmake USE_UPNP=- USE_IPV6=-
+    make
+
+    An executable named 'ecoin-qt' will be built.
+    
+    Now you can launch: ./ecoin-qt to run your ECOin wallet/GUI.
+       
+------------------------------   
+========================================
++ CPU MINER for ECOin (Unix/GNU-Linux) #
+========================================
+
+See doc/MINING.txt for detailed instructions on running /ecoin-miner/ on different platforms.
+
+  + GNU/Linux:
+
+	cd miner/
+	sh build.sh
+	
+    An executable named 'cpuminer' will be built.
+     
+    Now you can launch: ./cpuminer to run your ECOin PoW miner.
+
+======================================
+
+For a list of command-line options:
+
+  ./ecoind --help
+
+To start the ECOin daemon:
+
+  ./ecoind -daemon
+
+For ECOin-QT Wallet
+
+  ./ecoin-qt
+ 
+For debugging:
+
+  tail -f /home/$USER/.ecoin/debug.log
+
+======================================
+
+

+ 73 - 125
src/models/events_model.js

@@ -34,8 +34,6 @@ module.exports = ({ cooler }) => {
         createdAt: new Date().toISOString(),
         organizer: userId,
         status: 'OPEN',
-        opinions: {},
-        opinions_inhabitants: [],
         isPublic
       };
       return new Promise((resolve, reject) => {
@@ -88,111 +86,6 @@ module.exports = ({ cooler }) => {
       });
     },
 
-    async listAll(author = null, filter = 'all') {
-      const ssbClient = await openSsb();
-      return new Promise((resolve, reject) => {
-        pull(
-          ssbClient.createLogStream(),
-          pull.collect(async (err, results) => {
-            if (err) return reject(new Error("Error listing events: " + err.message));
-            const tombstoned = new Set();
-            const replaces = new Map();
-            const byId = new Map();
-            for (const r of results) {
-              const k = r.key;
-              const c = r.value.content;
-              if (!c) continue;
-              if (c.type === 'tombstone' && c.target) {
-                tombstoned.add(c.target);
-                continue;
-              }
-              if (c.type === 'event') {
-                if (tombstoned.has(k)) continue;
-                if (c.replaces) replaces.set(c.replaces, k);
-                if (author && c.organizer !== author) continue;
-                let status = c.status || 'OPEN';
-                const dateM = moment(c.date);
-                if (dateM.isValid() && dateM.isBefore(moment()) && status !== 'CLOSED') {
-                  const tombstone = {
-                    type: 'tombstone',
-                    target: k,
-                    deletedAt: new Date().toISOString(),
-                    author: c.organizer
-                  };
-                  const updated = {
-                    ...c,
-                    status: 'CLOSED',
-                    updatedAt: new Date().toISOString(),
-                    replaces: k
-                  };
-                  await new Promise((res, rej) => ssbClient.publish(tombstone, err => err ? rej(err) : res()));
-                  await new Promise((res, rej) => ssbClient.publish(updated, err => err ? rej(err) : res()));
-                  status = 'CLOSED';
-                }
-                byId.set(k, {
-                  id: k,
-                  title: c.title,
-                  description: c.description,
-                  date: c.date,
-                  location: c.location,
-                  price: c.price,
-                  url: c.url,
-                  attendees: c.attendees || [],
-                  tags: c.tags || [],
-                  createdAt: c.createdAt,
-                  organizer: c.organizer,
-                  status,
-                  opinions: c.opinions || {},
-                  opinions_inhabitants: c.opinions_inhabitants || [],
-                  isPublic: c.isPublic
-                });
-              }
-            }
-            for (const replaced of replaces.keys()) {
-              byId.delete(replaced);
-            }
-            let out = Array.from(byId.values());
-            if (filter === 'mine') out = out.filter(e => e.organizer === userId);
-            if (['features', 'bugs', 'abuse', 'content'].includes(filter)) out = out.filter(e => e.category === filter);
-            if (filter === 'confirmed') out = out.filter(e => e.confirmations?.length >= 3);
-            if (['open', 'resolved', 'invalid', 'underreview'].includes(filter)) out = out.filter(e => e.status.toLowerCase() === filter);
-            resolve(out);
-          })
-        );
-      });
-    },
-
-    async updateEventById(eventId, updatedData) {
-      const ssbClient = await openSsb();
-      return new Promise((resolve, reject) => {
-        ssbClient.get(eventId, (err, ev) => {
-          if (err || !ev || !ev.content) return reject(new Error("Error retrieving event"));
-          if (Object.keys(ev.content.opinions || {}).length > 0) return reject(new Error('Cannot edit event after it has received opinions'));
-          if (ev.content.organizer !== userId) return reject(new Error("Only the organizer can update this event"));
-          const tags = updatedData.tags ? updatedData.tags.split(',').map(t => t.trim()).filter(Boolean) : ev.content.tags;
-          const attendees = updatedData.attendees ? updatedData.attendees.split(',').map(t => t.trim()).filter(Boolean) : ev.content.attendees;
-          const tombstone = {
-            type: 'tombstone',
-            target: eventId,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          const updated = {
-            ...ev.content,
-            ...updatedData,
-            attendees,
-            tags,
-            updatedAt: new Date().toISOString(),
-            replaces: eventId
-          };
-          ssbClient.publish(tombstone, err => {
-            if (err) return reject(err);
-            ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
-          });
-        });
-      });
-    },
-
     async getEventById(eventId) {
       const ssbClient = await openSsb();
       return new Promise((resolve, reject) => {
@@ -230,45 +123,100 @@ module.exports = ({ cooler }) => {
             tags: c.tags || [],
             createdAt: c.createdAt || new Date().toISOString(),
             updatedAt: c.updatedAt || new Date().toISOString(),
-            opinions: c.opinions || {},
-            opinions_inhabitants: c.opinions_inhabitants || [],
             organizer: c.organizer || '',
             status,
-            isPublic: c.isPublic || 'private'
+            isPublic: c.isPublic || false
           });
         });
       });
     },
-
-    async createOpinion(id, category) {
+    
+    
+    async updateEventById(eventId, updatedData) {
       const ssbClient = await openSsb();
       return new Promise((resolve, reject) => {
-        ssbClient.get(id, (err, msg) => {
-          if (err || !msg || msg.content?.type !== 'event') return reject(new Error('Event not found'));
-          if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'));
+        ssbClient.get(eventId, (err, ev) => {
+          if (err || !ev || !ev.content) return reject(new Error("Error retrieving event"));
+          if (ev.content.organizer !== userId) return reject(new Error("Only the organizer can update this event"));
+          const tags = updatedData.tags ? updatedData.tags.split(',').map(t => t.trim()).filter(Boolean) : ev.content.tags;
+          const attendees = updatedData.attendees ? updatedData.attendees.split(',').map(t => t.trim()).filter(Boolean) : ev.content.attendees;
           const tombstone = {
             type: 'tombstone',
-            target: id,
+            target: eventId,
             deletedAt: new Date().toISOString(),
             author: userId
           };
           const updated = {
-            ...msg.content,
-            opinions: {
-              ...msg.content.opinions,
-              [category]: (msg.content.opinions?.[category] || 0) + 1
-            },
-            opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
+            ...ev.content,
+            ...updatedData,
+            attendees,
+            tags,
             updatedAt: new Date().toISOString(),
-            replaces: id
+            replaces: eventId
           };
           ssbClient.publish(tombstone, err => {
             if (err) return reject(err);
-            ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
+            ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
           });
         });
       });
-    }
+    },
+
+async listAll(author = null, filter = 'all') {
+  const ssbClient = await openSsb();
+  return new Promise((resolve, reject) => {
+    pull(
+      ssbClient.createLogStream(),
+      pull.collect((err, results) => {
+        if (err) return reject(new Error("Error listing events: " + err.message));
+        const tombstoned = new Set();
+        const replaces = new Map();
+        const byId = new Map();
+        for (const r of results) {
+          const k = r.key;
+          const c = r.value.content;
+          if (!c) continue;
+          if (c.type === 'tombstone' && c.target) {
+            tombstoned.add(c.target);
+            continue;
+          }
+          if (c.type === 'event') {
+            if (tombstoned.has(k)) continue;
+            if (c.replaces) replaces.set(c.replaces, k);
+            if (author && c.organizer !== author) continue;
+            let status = c.status || 'OPEN';
+            const dateM = moment(c.date);
+            if (dateM.isValid() && dateM.isBefore(moment())) status = 'CLOSED';
+            byId.set(k, {
+              id: k,
+              title: c.title,
+              description: c.description,
+              date: c.date,
+              location: c.location,
+              price: c.price,
+              url: c.url,
+              attendees: c.attendees || [],
+              tags: c.tags || [],
+              createdAt: c.createdAt,
+              organizer: c.organizer,
+              status,
+              isPublic: c.isPublic
+            });
+          }
+        }
+        for (const replaced of replaces.keys()) {
+          byId.delete(replaced);
+        }
+        let out = Array.from(byId.values());
+        if (filter === 'mine') out = out.filter(e => e.organizer === userId);
+        if (filter === 'open') out = out.filter(e => e.status === 'OPEN');
+        if (filter === 'closed') out = out.filter(e => e.status === 'CLOSED');
+        resolve(out);
+      })
+    );
+  });
+}
+    
   };
 };
 

+ 28 - 29
src/models/main_models.js

@@ -110,8 +110,8 @@ module.exports = ({ cooler, isPublic }) => {
     all_the_names = {};
 
     const allFeeds = Object.keys(feeds_to_name);
-    console.log(` - Synced-feeds: [ ${allFeeds.length} ]`);
-    console.time(" - Sync-time");
+    console.log(`- Synced-peers: [ ${allFeeds.length} ]`);
+    console.time("- Sync-time");
 
     const lookups = [];
     for (const feed of allFeeds) {
@@ -123,11 +123,11 @@ module.exports = ({ cooler, isPublic }) => {
       .then(() => {
         dirty = false; 
         running = false;
-        console.timeEnd(" - Sync-time");
+        console.timeEnd("- Sync-time");
       })
       .catch((err) => {
         running = false;
-        console.warn("lookup sync failed:", err);
+        console.warn("- Lookup Sync failed: ", err);
       });
   };
   const enhanceFeedInfo = ({ feed, name }) => {
@@ -1703,32 +1703,31 @@ const post = {
     inbox: async () => {
       const ssb = await cooler.open();
       const myFeedId = ssb.id;
-  const rawMessages = await new Promise((resolve, reject) => {
-    pull(
-      ssb.createLogStream({ reverse: true, limit: 1000 }),
-      pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))
-    );
-  });
-  const decryptedMessages = rawMessages.map(msg => {
-    try {
-      return ssb.private.unbox(msg);
-    } catch {
-      return null;
+      const rawMessages = await new Promise((resolve, reject) => {
+        pull(
+          ssb.createLogStream({ reverse: true, limit: 1000 }),
+          pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))
+        );
+      });
+     const decryptedMessages = rawMessages.map(msg => {
+        try {
+          return ssb.private.unbox(msg);
+        } catch {
+          return null;
+        }
+      }).filter(Boolean);
+      const tombstoneTargets = new Set(
+        decryptedMessages
+          .filter(msg => msg.value?.content?.type === 'tombstone')
+          .map(msg => msg.value.content.target)
+      );
+      return decryptedMessages.filter(msg => {
+        if (tombstoneTargets.has(msg.key)) return false;
+          const content = msg.value?.content;
+          const author = msg.value?.author;
+          return content?.type === 'post' && content?.private === true && (author === myFeedId || content.to?.includes(myFeedId));
+      });
     }
-  }).filter(Boolean);
-  const tombstoneTargets = new Set(
-    decryptedMessages
-      .filter(msg => msg.value?.content?.type === 'tombstone')
-      .map(msg => msg.value.content.target)
-  );
-  return decryptedMessages.filter(msg => {
-    if (tombstoneTargets.has(msg.key)) return false;
-    const content = msg.value?.content;
-    const author = msg.value?.author;
-    return content?.type === 'post' && content?.private === true && (author === myFeedId || content.to?.includes(myFeedId));
-  });
-}
-
 
   };
   models.post = post;

+ 191 - 133
src/models/market_model.js

@@ -8,39 +8,40 @@ module.exports = ({ cooler }) => {
   return {
     type: 'market',
 
-    async createItem(item_type, title, description, image, price, tagsRaw = [], item_status, deadline, includesShipping = false) {
-      const ssbClient = await openSsb();
-      const userId = ssbClient.id;
-      const formattedDeadline = deadline ? moment(deadline, moment.ISO_8601, true).toISOString() : null;
-      let blobId = null;
-      if (image) {
-        const match = image.match(/\(([^)]+)\)/);
-        blobId = match ? match[1] : image;
-      }
-      const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
-      const itemContent = {
-        type: "market",
-        item_type,
-        title,
-        description,
-        image: blobId,
-        price: parseFloat(price).toFixed(6),
-        tags,
-        item_status,
-        status: 'FOR SALE',
-        deadline: formattedDeadline,
-        includesShipping,
-        createdAt: new Date().toISOString(),
-        updatedAt: new Date().toISOString(),
-        seller: userId,
-        auctions_poll: []
-      };
-      return new Promise((resolve, reject) => {
-        ssbClient.publish(itemContent, (err, res) => err ? reject(err) : resolve(res));
-      });
-    },
+  async createItem(item_type, title, description, image, price, tagsRaw = [], item_status, deadline, includesShipping = false, stock = 0) {
+    const ssbClient = await openSsb();
+    const userId = ssbClient.id;
+    const formattedDeadline = deadline ? moment(deadline, moment.ISO_8601, true).toISOString() : null;
+    let blobId = null;
+    if (image) {
+      const match = image.match(/\(([^)]+)\)/);
+      blobId = match ? match[1] : image;
+    }
+    const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
+    const itemContent = {
+      type: "market",
+      item_type,
+      title,
+      description,
+      image: blobId,
+      price: parseFloat(price).toFixed(6),
+      tags,
+      item_status,
+      status: 'FOR SALE',
+      deadline: formattedDeadline,
+      includesShipping,
+      stock,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      seller: userId,
+      auctions_poll: []
+    };
+    return new Promise((resolve, reject) => {
+      ssbClient.publish(itemContent, (err, res) => err ? reject(err) : resolve(res));
+    });
+  },
 
-    async updateItemById(itemId, updatedData) {
+   async updateItemById(itemId, updatedData) {
       const ssbClient = await openSsb();
       const userId = ssbClient.id;
       return new Promise((resolve, reject) => {
@@ -90,75 +91,96 @@ module.exports = ({ cooler }) => {
     async listAllItems(filter = 'all') {
       const ssbClient = await openSsb();
       const userId = ssbClient.id;
-	  return new Promise((resolve, reject) => {
-	    pull(
-	      ssbClient.createLogStream(),
-	      pull.collect(async (err, results) => {
-		if (err) return reject(new Error("Error listing items: " + err.message));
-		const tombstoned = new Set();
-		const replaces = new Map();
-		const itemsById = new Map();
-
-		for (const msg of results) {
-		  const c = msg.value?.content;
-		  const k = msg.key;
-		  if (!c) continue;
-		  if (c.type === 'tombstone' && c.target) {
-		    tombstoned.add(c.target);
-		    continue;
-		  }
-		  if (c.type === 'market') {
-		    if (tombstoned.has(k)) continue;
-		    if (c.replaces) replaces.set(c.replaces, k);
-		    itemsById.set(k, { id: k, ...c });
-		  }
-		}
-
-		for (const replacedId of replaces.keys()) {
-		  itemsById.delete(replacedId);
-		}
-
-		let filteredItems = Array.from(itemsById.values());
-		await this.checkAuctionItemsStatus(filteredItems);
-
-		switch (filter) {
-		  case 'mine':
-		    filteredItems = filteredItems.filter(e => e.seller === userId);
-		    break;
-		  case 'exchange':
-		    filteredItems = filteredItems.filter(e => e.item_type === 'exchange' && e.status === 'FOR SALE');
-		    break;
-		  case 'auctions':
-		    filteredItems = filteredItems.filter(e => e.item_type === 'auction' && e.status === 'FOR SALE');
-		    break;
-		  case 'new':
-		    filteredItems = filteredItems.filter(e => e.item_status === 'NEW' && e.status === 'FOR SALE');
-		    break;
-		  case 'used':
-		    filteredItems = filteredItems.filter(e => e.item_status === 'USED' && e.status === 'FOR SALE');
-		    break;
-		  case 'broken':
-		    filteredItems = filteredItems.filter(e => e.item_status === 'BROKEN' && e.status === 'FOR SALE');
-		    break;
-		  case 'for sale':
-		    filteredItems = filteredItems.filter(e => e.status === 'FOR SALE');
-		    break;
-		  case 'sold':
-		    filteredItems = filteredItems.filter(e => e.status === 'SOLD');
-		    break;
-		  case 'discarded':
-		    filteredItems = filteredItems.filter(e => e.status === 'DISCARDED');
-		    break;
-		  default:
-		    break;
-		}
-
-		filteredItems = filteredItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
-		resolve(filteredItems);
-	      })
-	    );
-	  });
-	},
+      return new Promise((resolve, reject) => {
+        pull(
+          ssbClient.createLogStream(),
+          pull.collect(async (err, results) => {
+            if (err) return reject(new Error("Error listing items: " + err.message));
+            const tombstoned = new Set();
+            const replaces = new Map();
+            const itemsById = new Map();
+            const now = moment();
+            for (const r of results) {
+              const k = r.key;
+              const c = r.value.content;
+              if (!c) continue;
+              if (c.type === 'tombstone' && c.target) {
+                tombstoned.add(c.target);
+                continue;
+              }
+              if (c.type === 'market') {
+                if (tombstoned.has(k)) continue;
+                if (c.replaces) replaces.set(c.replaces, k);
+                let status = c.status || 'FOR SALE';
+                if (c.deadline) {
+                  const deadline = moment(c.deadline);
+                  if (deadline.isValid() && deadline.isBefore(now) && status !== 'SOLD') {
+                    status = 'DISCARDED';
+                  }
+                }
+                if (c.stock === 0 && c.status === 'FOR SALE') continue;
+                itemsById.set(k, {
+                  id: k,
+                  title: c.title,
+                  description: c.description,
+                  image: c.image,
+                  price: c.price,
+                  tags: c.tags || [],
+                  item_status: c.item_status || 'NEW',
+                  status,
+                  createdAt: c.createdAt,
+                  updatedAt: c.updatedAt,
+                  seller: c.seller,
+                  includesShipping: c.includesShipping || false,
+                  stock: c.stock || 0,
+                  deadline: c.deadline,
+                  auctions_poll: c.auctions_poll || [],
+                  item_type: c.item_type
+                });
+              }
+            }
+            for (const replacedId of replaces.keys()) {
+              itemsById.delete(replacedId);
+            }
+            let filteredItems = Array.from(itemsById.values());
+            switch (filter) {
+              case 'mine':
+                filteredItems = filteredItems.filter(e => e.seller === userId);
+                break;
+              case 'exchange':
+                filteredItems = filteredItems.filter(e => e.item_type === 'exchange' && e.status === 'FOR SALE');
+                break;
+              case 'auctions':
+                filteredItems = filteredItems.filter(e => e.item_type === 'auction' && e.status === 'FOR SALE');
+                break;
+              case 'new':
+                filteredItems = filteredItems.filter(e => e.item_status === 'NEW' && e.status === 'FOR SALE');
+                break;
+              case 'used':
+                filteredItems = filteredItems.filter(e => e.item_status === 'USED' && e.status === 'FOR SALE');
+                break;
+              case 'broken':
+                filteredItems = filteredItems.filter(e => e.item_status === 'BROKEN' && e.status === 'FOR SALE');
+                break;
+              case 'for sale':
+                filteredItems = filteredItems.filter(e => e.status === 'FOR SALE');
+                break;
+              case 'sold':
+                filteredItems = filteredItems.filter(e => e.status === 'SOLD');
+                break;
+              case 'discarded':
+                filteredItems = filteredItems.filter(e => e.status === 'DISCARDED');
+                break;
+              default:
+                break;
+            }
+            filteredItems = filteredItems.filter(item => !(item.status === 'FOR SALE' && item.stock === 0));
+            filteredItems = filteredItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+            resolve(filteredItems);
+          })
+        );
+      });
+    },
 
     async checkAuctionItemsStatus(items) {
       const now = new Date().toISOString();
@@ -180,25 +202,30 @@ module.exports = ({ cooler }) => {
       }
     },
 
-    async setItemAsSold(itemId) {
+  async setItemAsSold(itemId) {
       const ssbClient = await openSsb();
       const userId = ssbClient.id;
       return new Promise((resolve, reject) => {
         ssbClient.get(itemId, (err, item) => {
           if (err || !item?.content) return reject(new Error("Item not found"));
           if (['SOLD', 'DISCARDED'].includes(item.content.status)) return reject(new Error("Already sold/discarded"));
+          if (item.content.stock <= 0) return reject(new Error("Out of stock"));
+
           const updated = {
             ...item.content,
+            stock: 0,
             status: 'SOLD',
             updatedAt: new Date().toISOString(),
             replaces: itemId
           };
+
           const tombstone = {
             type: 'tombstone',
             target: itemId,
             deletedAt: new Date().toISOString(),
             author: userId
           };
+
           ssbClient.publish(tombstone, (err) => {
             if (err) return reject(err);
             ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
@@ -234,6 +261,7 @@ module.exports = ({ cooler }) => {
             title: c.title,
             description: c.description,
             price: c.price,
+            stock: c.stock,
             status,
             item_status: c.item_status,
             seller: c.seller,
@@ -249,40 +277,70 @@ module.exports = ({ cooler }) => {
         });
       });
     },
+    
+  async addBidToAuction(itemId, userId, bidAmount) {
+    const ssbClient = await openSsb();
+    return new Promise((resolve, reject) => {
+    ssbClient.get(itemId, (err, item) => {
+      if (err || !item?.content) return reject(new Error("Item not found"));
+      if (item.content.item_type !== 'auction') return reject(new Error("Not an auction"));
+      if (item.content.seller === userId) return reject(new Error("Cannot bid on your own item"));
+      if (parseFloat(bidAmount) <= parseFloat(item.content.price)) return reject(new Error("Bid too low"));
+      const highestBid = item.content.auctions_poll.reduce((prev, curr) => {
+        const [_, bid] = curr.split(':');
+        return Math.max(prev, parseFloat(bid));
+      }, 0);
+      if (parseFloat(bidAmount) <= highestBid) return reject(new Error("Bid not highest"));
+      const bid = `${userId}:${bidAmount}:${new Date().toISOString()}`;
+      const updated = {
+        ...item.content,
+        auctions_poll: [...(item.content.auctions_poll || []), bid],
+        stock: item.content.stock - 1,
+        updatedAt: new Date().toISOString(),
+        replaces: itemId
+      };
+      const tombstone = {
+        type: 'tombstone',
+        target: itemId,
+        deletedAt: new Date().toISOString(),
+        author: userId
+      };
+      ssbClient.publish(tombstone, (err) => {
+        if (err) return reject(err);
+        ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
+      });
+     });
+    });
+  },
+  
+  async decrementStock(itemId) {
+    const ssbClient = await openSsb();
+    const userId = ssbClient.id;
+    return new Promise((resolve, reject) => {
+      ssbClient.get(itemId, (err, item) => {
+        if (err || !item?.content) return reject(new Error("Item not found"));
+        if (item.content.stock <= 0) return reject(new Error("No stock left"));
+        const updated = {
+          ...item.content,
+          stock: item.content.stock - 1,
+          updatedAt: new Date().toISOString(),
+          replaces: itemId
+        };
 
-    async addBidToAuction(itemId, userId, bidAmount) {
-      const ssbClient = await openSsb();
-      return new Promise((resolve, reject) => {
-        ssbClient.get(itemId, (err, item) => {
-          if (err || !item?.content) return reject(new Error("Item not found"));
-          if (item.content.item_type !== 'auction') return reject(new Error("Not an auction"));
-          if (item.content.seller === userId) return reject(new Error("Cannot bid on your own item"));
-          if (parseFloat(bidAmount) <= parseFloat(item.content.price)) return reject(new Error("Bid too low"));
-          const highestBid = item.content.auctions_poll.reduce((prev, curr) => {
-            const [_, bid] = curr.split(':');
-            return Math.max(prev, parseFloat(bid));
-          }, 0);
-          if (parseFloat(bidAmount) <= highestBid) return reject(new Error("Bid not highest"));
-          const bid = `${userId}:${bidAmount}:${new Date().toISOString()}`;
-          const updated = {
-            ...item.content,
-            auctions_poll: [...(item.content.auctions_poll || []), bid],
-            updatedAt: new Date().toISOString(),
-            replaces: itemId
-          };
-          const tombstone = {
-            type: 'tombstone',
-            target: itemId,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          ssbClient.publish(tombstone, (err) => {
-            if (err) return reject(err);
-            ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
-          });
+        const tombstone = {
+          type: 'tombstone',
+          target: itemId,
+          deletedAt: new Date().toISOString(),
+          author: userId
+        };
+        ssbClient.publish(tombstone, (err) => {
+          if (err) return reject(err);
+          ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
         });
       });
-    }
+    });
+  }
+  
   };
 };
 

+ 10 - 6
src/models/opinions_model.js

@@ -13,14 +13,12 @@ module.exports = ({ cooler }) => {
   ];
 
   const validTypes = [
-    'event', 'bookmark', 'task', 'votes', 'report', 'transfer',
+    'bookmark', 'votes', 'transfer',
     'feed', 'image', 'audio', 'video', 'document'
   ];
 
   const getPreview = (c) => {
     if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`;
-    if (c.type === 'event' && c.text) return c.text.slice(0, 200);
-    if (c.type === 'task' && c.description) return c.description;
     return c.text || c.description || c.title || '';
   };
 
@@ -34,8 +32,10 @@ module.exports = ({ cooler }) => {
     );
 
     if (!msg || !msg.content) throw new Error("Opinion not found.");
-    const type = msg.content.type;
-    if (!validTypes.includes(type)) throw new Error("Invalid content type for voting.");
+	const type = msg.content.type;
+	if (!validTypes.includes(type) || ['task', 'event', 'report'].includes(type)) {
+	  throw new Error("Voting not allowed on this content type.");
+	}
     if (msg.content.opinions_inhabitants?.includes(userId)) throw new Error("Already voted.");
 
     const tombstone = {
@@ -87,7 +87,11 @@ module.exports = ({ cooler }) => {
         tombstoned.add(c.target);
         continue;
       }
-      if (c.opinions && !tombstoned.has(key)) {
+       if (
+	  c.opinions &&
+	  !tombstoned.has(key) &&
+	  !['task', 'event', 'report'].includes(c.type)
+	) {
         if (c.replaces) replaces.set(c.replaces, key);
         byId.set(key, {
           key,

+ 1 - 6
src/models/privatemessages_model.js

@@ -11,10 +11,9 @@ module.exports = ({ cooler }) => {
     }
     return ssb;
   };
-
+  
   return {
     type: 'post',
-
     async sendMessage(recipients = [], subject = '', text = '') {
       const ssbClient = await openSsb();
       const content = {
@@ -47,14 +46,12 @@ module.exports = ({ cooler }) => {
       } catch {
         throw new Error("Malformed message.");
       }
-
       const content = decrypted?.value?.content;
       const author = decrypted?.value?.author;
       const recps = content?.to;
 
       if (!content || !author || !Array.isArray(recps)) throw new Error("Malformed message.");
       if (content.type === 'tombstone') throw new Error("Message already deleted.");
-      if (author !== userId) throw new Error("You are not the author of this message.");
 
       const tombstone = {
         type: 'tombstone',
@@ -62,10 +59,8 @@ module.exports = ({ cooler }) => {
         deletedAt: new Date().toISOString(),
         private: true
       };
-
       const publishAsync = util.promisify(ssbClient.private.publish);
       return publishAsync(tombstone, recps);
     }
   };
 };
-

+ 89 - 179
src/models/reports_model.js

@@ -1,24 +1,23 @@
-const pull = require('../server/node_modules/pull-stream')
-const moment = require('../server/node_modules/moment')
+const pull = require('../server/node_modules/pull-stream');
+const moment = require('../server/node_modules/moment');
 
 module.exports = ({ cooler }) => {
-  let ssb
-  const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
+  let ssb;
+  const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
 
   return {
-    type: 'report',
-
-    async createReport(title, description, category, image, tagsRaw = [], severity = 'low', isAnonymous = false) {
-      const ssb = await openSsb()
-      const userId = ssb.id
-      let blobId = null
+    async createReport(title, description, category, image, tagsRaw = [], severity = 'low') {
+      const ssb = await openSsb();
+      const userId = ssb.id;
+      let blobId = null;
       if (image) {
-        const match = image.match(/\(([^)]+)\)/)
-        blobId = match ? match[1] : image
+        const match = image.match(/\(([^)]+)\)/);
+        blobId = match ? match[1] : image;
       }
       const tags = Array.isArray(tagsRaw)
         ? tagsRaw.filter(Boolean)
-        : tagsRaw.split(',').map(t => t.trim()).filter(Boolean)
+        : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
+
       const content = {
         type: 'report',
         title,
@@ -28,188 +27,99 @@ module.exports = ({ cooler }) => {
         author: userId,
         image: blobId,
         tags,
-        opinions: {},
-        opinions_inhabitants: [],
         confirmations: [],
         severity,
-        status: 'OPEN',
-        isAnonymous
-      }
-      return new Promise((resolve, reject) => {
-        ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))
-      })
+        status: 'OPEN'
+      };
+
+      return new Promise((res, rej) => ssb.publish(content, (err, msg) => err ? rej(err) : res(msg)));
     },
 
     async updateReportById(id, updatedContent) {
-      const ssb = await openSsb()
-      const userId = ssb.id
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, report) => {
-          if (err || !report || !report.content) return reject(new Error('Report not found'))
-          if (report.content.author !== userId) return reject(new Error('Not the author'))
-          if (Object.keys(report.content.opinions || {}).length > 0) return reject(new Error('Cannot edit report after it has received opinions.'))
-          const tags = updatedContent.tags
-            ? updatedContent.tags.split(',').map(t => t.trim()).filter(Boolean)
-            : report.content.tags
-          let blobId = null
-          if (updatedContent.image) {
-            const match = updatedContent.image.match(/\(([^)]+)\)/)
-            blobId = match ? match[1] : updatedContent.image
-          }
-          const updated = {
-            ...report.content,
-            ...updatedContent,
-            type: 'report',
-            replaces: id,
-            image: blobId || report.content.image,
-            tags,
-            updatedAt: new Date().toISOString(),
-            author: report.content.author
-          }
-          ssb.publish(updated, (e, r) => e ? reject(e) : resolve(r))
-        })
-      })
-    },
+      const ssb = await openSsb();
+      const userId = ssb.id;
+      const report = await new Promise((res, rej) => ssb.get(id, (err, report) => err ? rej(new Error('Report not found')) : res(report)));
+      if (report.content.author !== userId) throw new Error('Not the author');
 
-    async deleteReportById(id) {
-      const ssb = await openSsb()
-      const userId = ssb.id
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, report) => {
-          if (err || !report || !report.content) return reject(new Error('Report not found'))
-          if (report.content.author !== userId) return reject(new Error('Not the author'))
-          const tombstone = {
-            type: 'tombstone',
-            target: id,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          }
-          ssb.publish(tombstone, (err, res) => err ? reject(err) : resolve(res))
-        })
-      })
+      const tags = updatedContent.tags
+        ? updatedContent.tags.split(',').map(t => t.trim()).filter(Boolean)
+        : report.content.tags;
+
+      let blobId = report.content.image;
+      if (updatedContent.image) {
+        const match = updatedContent.image.match(/\(([^)]+)\)/);
+        blobId = match ? match[1] : updatedContent.image;
+      }
+
+      const updated = {
+        ...report.content,
+        ...updatedContent,
+        type: 'report',
+        replaces: id,
+        image: blobId,
+        tags,
+        updatedAt: new Date().toISOString(),
+        author: report.content.author
+      };
+
+      return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
     },
 
-    async listAll(filter = 'all') {
-      const ssb = await openSsb()
-      const userId = ssb.id
-      return new Promise((resolve, reject) => {
-        pull(
-          ssb.createLogStream(),
-          pull.collect((err, results) => {
-            if (err) return reject(err)
-            const tombstonedIds = new Set(
-              results
-                .filter(msg => msg.value.content?.type === 'tombstone')
-                .map(msg => msg.value.content.target)
-            )
-            const replaces = new Map()
-            const latest = new Map()
-            for (const msg of results) {
-              const k = msg.key
-              const c = msg.value?.content
-              if (!c || c.type !== 'report') continue
-              if (tombstonedIds.has(k)) continue
-              if (c.replaces) replaces.set(c.replaces, k)
-              latest.set(k, {
-                id: k,
-                title: c.title,
-                description: c.description,
-                category: c.category,
-                createdAt: c.createdAt,
-                author: c.author,
-                image: c.image || null,
-                tags: c.tags || [],
-                opinions: c.opinions || {},
-                opinions_inhabitants: c.opinions_inhabitants || [],
-                confirmations: c.confirmations || [],
-                severity: c.severity || 'LOW',
-                status: c.status || 'OPEN',
-                isAnonymous: c.isAnonymous || false
-              })
-            }
-            for (const oldId of replaces.keys()) {
-              latest.delete(oldId)
-            }
-            let reports = Array.from(latest.values())
-            if (filter === 'mine') reports = reports.filter(r => r.author === userId)
-            if (['features', 'bugs', 'abuse', 'content'].includes(filter)) {
-              reports = reports.filter(r => r.category.toLowerCase() === filter)
-            }
-            if (filter === 'confirmed') reports = reports.filter(r => r.confirmations.length >= 3)
-            if (['open', 'resolved', 'invalid', 'underreview'].includes(filter)) {
-              reports = reports.filter(r => r.status.toLowerCase() === filter)
-            }
-            resolve(reports)
-          })
-        )
-      })
+    async deleteReportById(id) {
+      const ssb = await openSsb();
+      const userId = ssb.id;
+      const report = await new Promise((res, rej) => ssb.get(id, (err, report) => err ? rej(new Error('Report not found')) : res(report)));
+      if (report.content.author !== userId) throw new Error('Not the author');
+      const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
+      return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result)));
     },
 
     async getReportById(id) {
-      const ssb = await openSsb()
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, report) => {
-          if (err || !report || !report.content) return reject(new Error('Report not found'))
-          const c = report.content
-          resolve({
-            id,
-            title: c.title,
-            description: c.description,
-            category: c.category,
-            createdAt: c.createdAt,
-            author: c.author,
-            image: c.image || null,
-            tags: c.tags || [],
-            opinions: c.opinions || {},
-            opinions_inhabitants: c.opinions_inhabitants || [],
-            confirmations: c.confirmations || [],
-            severity: c.severity || 'LOW',
-            status: c.status || 'OPEN',
-            isAnonymous: c.isAnonymous || false
-          })
-        })
-      })
+      const ssb = await openSsb();
+      const report = await new Promise((res, rej) => ssb.get(id, (err, report) => err ? rej(new Error('Report not found')) : res(report)));
+      const c = report.content;
+      return { id, ...c };
     },
 
     async confirmReportById(id) {
-      const ssb = await openSsb()
-      const userId = ssb.id
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, report) => {
-          if (err || !report || !report.content) return reject(new Error('Report not found'))
-          if ((report.content.confirmations || []).includes(userId)) return reject(new Error('Already confirmed'))
-          const updated = {
-            ...report.content,
-            replaces: id,
-            confirmations: [...(report.content.confirmations || []), userId],
-            updatedAt: new Date().toISOString()
-          }
-          ssb.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result))
-        })
-      })
+      const ssb = await openSsb();
+      const userId = ssb.id;
+      const report = await new Promise((res, rej) => ssb.get(id, (err, report) => err ? rej(new Error('Report not found')) : res(report)));
+      if (report.content.confirmations.includes(userId)) throw new Error('Already confirmed');
+      const updated = {
+        ...report.content,
+        replaces: id,
+        confirmations: [...report.content.confirmations, userId],
+        updatedAt: new Date().toISOString()
+      };
+      return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
     },
 
-    async createOpinion(id, category) {
-      const ssbClient = await openSsb()
-      const userId = ssbClient.id
+    async listAll() {
+      const ssb = await openSsb();
       return new Promise((resolve, reject) => {
-        ssbClient.get(id, (err, msg) => {
-          if (err || !msg || msg.content?.type !== 'report') return reject(new Error('Report not found'))
-          if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'))
-          const updated = {
-            ...msg.content,
-            replaces: id,
-            opinions: {
-              ...msg.content.opinions,
-              [category]: (msg.content.opinions?.[category] || 0) + 1
-            },
-            opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
-            updatedAt: new Date().toISOString()
+        pull(ssb.createLogStream(), pull.collect((err, results) => {
+          if (err) return reject(err);
+          const tombstoned = new Set();
+          const replaced = new Map();
+          const reports = new Map();
+
+          for (const r of results) {
+            const { key, value: { content: c } } = r;
+            if (!c) continue;
+            if (c.type === 'tombstone') tombstoned.add(c.target);
+            if (c.type === 'report') {
+              if (c.replaces) replaced.set(c.replaces, key);
+              reports.set(key, { id: key, ...c });
+            }
           }
-          ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result))
-        })
-      })
-    }
-  }
-}
 
+          tombstoned.forEach(id => reports.delete(id));
+          replaced.forEach((_, oldId) => reports.delete(oldId));
+
+          resolve([...reports.values()]);
+        }));
+      });
+    }
+  };
+};

+ 6 - 3
src/models/search_model.js

@@ -10,7 +10,7 @@ module.exports = ({ cooler }) => {
 
   const searchableTypes = [
     'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed',
-    'vote', 'report', 'task', 'event', 'bookmark', 'document',
+    'votes', 'report', 'task', 'event', 'bookmark', 'document',
     'image', 'audio', 'video', 'market'
   ];
 
@@ -24,8 +24,8 @@ module.exports = ({ cooler }) => {
         return [content?.text, content?.author, content?.createdAt, ...(content?.tags || []), content?.refeeds];
       case 'event':
         return [content?.title, content?.description, content?.date, content?.location, content?.price, content?.eventUrl, ...(content?.tags || []), content?.attendees, content?.organizer, content?.status, content?.isPublic];
-      case 'vote':
-        return [content?.question, content?.deadline, content?.status, ...(content?.votes || []), content?.totalVotes];
+      case 'votes':
+        return [content?.question, content?.deadline, content?.status, ...(Object.values(content?.votes || {})), content?.totalVotes];
       case 'tribe':
         return [content?.title, content?.description, content?.image, content?.location, ...(content?.tags || []), content?.isLARP, content?.isAnonymous, content?.members?.length, content?.createdAt, content?.author];
       case 'audio':
@@ -86,6 +86,9 @@ module.exports = ({ cooler }) => {
       const c = msg?.value?.content;
       const t = c?.type;
       if (!t || (types.length > 0 && !types.includes(t))) return false;
+      if (t === 'market') {
+        if (c.stock === 0 && c.status !== 'SOLD') return false;
+      }
       if (query.startsWith('@') && query.length > 1) return (t === 'about' && c?.about === query);
       const fields = getRelevantFields(t, c);
       if (query.startsWith('#') && query.length > 1) {

+ 14 - 12
src/models/tags_model.js

@@ -11,27 +11,32 @@ module.exports = ({ cooler }) => {
   return {
     async listTags(filter = 'all') {
       const ssbClient = await openSsb();
-
       return new Promise((resolve, reject) => {
         pull(
           ssbClient.createLogStream(),
           pull.filter(msg => {
             const c = msg.value.content;
-            return c && Array.isArray(c.tags) && c.tags.length && c.type !== 'tombstone';
+            return c && Array.isArray(c.tags) && c.tags.length && c.type !== 'tombstone'; 
           }),
           pull.collect((err, results) => {
             if (err) return reject(new Error(`Error retrieving tags: ${err.message}`));
             const counts = {};
-
+            const seenKeys = new Set(); 
             results.forEach(record => {
               const c = record.value.content;
-              c.tags.filter(Boolean).forEach(tag => {
-                counts[tag] = (counts[tag] || 0) + 1;
-              });
+              const key = record.key; 
+              const timestamp = c.timestamp;
+              if (c.replaces && seenKeys.has(c.replaces)) {
+                return;
+              }
+              if (!seenKeys.has(key)) {
+                seenKeys.add(key); 
+                c.tags.filter(Boolean).forEach(tag => {
+                  counts[tag] = (counts[tag] || 0) + 1;
+                });
+              }
             });
-
             let tags = Object.entries(counts).map(([name, count]) => ({ name, count }));
-
             if (filter === 'top') {
               tags.sort((a, b) => b.count - a.count);
             } else if (filter === 'cloud') {
@@ -40,10 +45,7 @@ module.exports = ({ cooler }) => {
             } else {
               tags.sort((a, b) => a.name.localeCompare(b.name));
             }
-
-            const deduplicatedTags = Array.from(new Map(tags.map(tag => [tag.name, tag])).values());
-
-            resolve(deduplicatedTags);
+            resolve(tags);
           })
         );
       });

+ 66 - 174
src/models/tasks_model.js

@@ -6,18 +6,15 @@ module.exports = ({ cooler }) => {
   const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
 
   return {
-    type: 'task',
-
     async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic) {
-      const ssbClient = await openSsb();
-      const userId = ssbClient.id;
+      const ssb = await openSsb();
+      const userId = ssb.id;
       const start = moment(startTime);
       const end = moment(endTime);
-      if (!start.isValid()) throw new Error('Invalid starting date');
-      if (!end.isValid()) throw new Error('Invalid ending date');
-      if (start.isBefore(moment())) throw new Error('Start time is in the past');
-      if (end.isBefore(start)) throw new Error('End time is before start time');
+      if (!start.isValid() || !end.isValid()) throw new Error('Invalid dates');
+      if (start.isBefore(moment()) || end.isBefore(start)) throw new Error('Invalid time range');
       const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
+
       const content = {
         type: 'task',
         title,
@@ -31,21 +28,52 @@ module.exports = ({ cooler }) => {
         assignees: [userId],
         createdAt: new Date().toISOString(),
         status: 'OPEN',
-        author: userId,
-        opinions: {},
-        opinions_inhabitants: []
+        author: userId
       };
-      return new Promise((resolve, reject) => {
-        ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res));
-      });
+
+      return new Promise((res, rej) => ssb.publish(content, (err, msg) => err ? rej(err) : res(msg)));
+    },
+
+    async deleteTaskById(taskId) {
+      const ssb = await openSsb();
+      const userId = ssb.id;
+      const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task)));
+      if (task.content.author !== userId) throw new Error('Not the author');
+      const tombstone = { type: 'tombstone', target: taskId, deletedAt: new Date().toISOString(), author: userId };
+      return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result)));
+    },
+
+    async updateTaskById(taskId, updatedData) {
+      const ssb = await openSsb();
+      const userId = ssb.id;
+      const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task)));
+      if (task.content.status === 'CLOSED') throw new Error('Cannot edit a closed task');
+      const updated = { ...task.content, ...updatedData, updatedAt: new Date().toISOString(), replaces: taskId };
+      const tombstone = { type: 'tombstone', target: taskId, deletedAt: new Date().toISOString(), author: userId };
+      await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
+      return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
+    },
+
+    async updateTaskStatus(taskId, status) {
+      if (!['OPEN', 'IN-PROGRESS', 'CLOSED'].includes(status)) throw new Error('Invalid status');
+      return this.updateTaskById(taskId, { status });
+    },
+
+    async getTaskById(taskId) {
+      const ssb = await openSsb();
+      const now = moment();
+      const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task)));
+      const c = task.content;
+      const status = c.status === 'OPEN' && moment(c.endTime).isBefore(now) ? 'CLOSED' : c.status;
+      return { id: taskId, ...c, status };
     },
 
     async toggleAssignee(taskId) {
-      const ssbClient = await openSsb();
-      const userId = ssbClient.id;
+      const ssb = await openSsb();
+      const userId = ssb.id;
       const task = await this.getTaskById(taskId);
-      let assignees = Array.isArray(task.assignees) ? [...task.assignees] : [];
       if (task.status === 'CLOSED') throw new Error('Cannot assign users to a closed task');
+      let assignees = Array.isArray(task.assignees) ? [...task.assignees] : [];
       const idx = assignees.indexOf(userId);
       if (idx !== -1) {
         assignees.splice(idx, 1);
@@ -55,168 +83,32 @@ module.exports = ({ cooler }) => {
       return this.updateTaskById(taskId, { assignees });
     },
 
-    async updateTaskStatus(taskId, status) {
-      if (!['OPEN', 'IN-PROGRESS', 'CLOSED'].includes(status)) throw new Error('Invalid status');
-      return this.updateTaskById(taskId, { status });
-    },
-
-    async deleteTaskById(taskId) {
-      const ssbClient = await openSsb();
-      const userId = ssbClient.id;
+    async listAll() {
+      const ssb = await openSsb();
+      const now = moment();
       return new Promise((resolve, reject) => {
-        ssbClient.get(taskId, (err, task) => {
-          if (err || !task || !task.content) return reject(new Error('Task not found'));
-          if (task.content.author !== userId) return reject(new Error('Only the author can delete the task'));
-          const tombstone = {
-            type: 'tombstone',
-            target: taskId,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          ssbClient.publish(tombstone, (pubErr, res) => pubErr ? reject(pubErr) : resolve(res));
-        });
-      });
-    },
-
-  async updateTaskById(taskId, updatedData) {
-    const ssbClient = await openSsb();
-    const userId = ssbClient.id;
-
-    return new Promise((resolve, reject) => {
-      ssbClient.get(taskId, (err, task) => {
-        if (err || !task || !task.content) return reject(new Error('Task not found'));
-        if (Object.keys(task.content.opinions || {}).length > 0) return reject(new Error('Cannot edit task after it has received opinions.'));
-        if (task.content.status === 'CLOSED') return reject(new Error('Cannot edit a closed task.'));
-        if (updatedData.tags) {
-          updatedData.tags = Array.isArray(updatedData.tags) ? updatedData.tags : updatedData.tags.split(',').map(tag => tag.trim());
-        }
-
-        const tombstone = {
-          type: 'tombstone',
-          target: taskId,
-          deletedAt: new Date().toISOString(),
-          author: userId
-        };
-
-        const updated = {
-          ...task.content,
-          ...updatedData,
-          updatedAt: new Date().toISOString(),
-          replaces: taskId
-        };
-
-        ssbClient.publish(tombstone, (err) => {
+        pull(ssb.createLogStream(), pull.collect((err, results) => {
           if (err) return reject(err);
-          ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
-        });
-      });
-    });
-  },
+          const tombstoned = new Set();
+          const replaced = new Map();
+          const tasks = new Map();
 
-    async getTaskById(taskId) {
-      const ssbClient = await openSsb();
-      return new Promise((resolve, reject) => {
-        ssbClient.get(taskId, (err, task) => {
-          if (err || !task || !task.content || task.content.type === 'tombstone') return reject(new Error('Task not found'));
-          const c = task.content;
-          resolve({
-            id: taskId,
-            title: c.title,
-            description: c.description,
-            startTime: c.startTime,
-            endTime: c.endTime,
-            priority: c.priority,
-            location: c.location,
-            tags: Array.isArray(c.tags) ? c.tags : [],
-            isPublic: c.isPublic,
-            assignees: Array.isArray(c.assignees) ? c.assignees : [],
-            createdAt: c.createdAt,
-            status: c.status || 'OPEN',
-            author: c.author,
-            opinions: c.opinions || {},
-            opinions_inhabitants: c.opinions_inhabitants || []
-          });
-        });
-      });
-    },
-
-    async listAll(filter = 'all') {
-      const ssbClient = await openSsb();
-      return new Promise((resolve, reject) => {
-        pull(
-          ssbClient.createLogStream(),
-          pull.collect((err, results) => {
-            if (err) return reject(err);
-            const tombstoned = new Set();
-            const replaces = new Map();
-            const byId = new Map();
-            for (const msg of results) {
-              const k = msg.key;
-              const c = msg.value.content;
-              if (!c) continue;
-              if (c.type === 'tombstone' && c.target) {
-                tombstoned.add(c.target);
-                continue;
-              }
-              if (c.type === 'task') {
-                if (tombstoned.has(k)) continue;
-                if (c.replaces) replaces.set(c.replaces, k);
-                byId.set(k, {
-                  id: k,
-                  title: c.title,
-                  description: c.description,
-                  startTime: c.startTime,
-                  endTime: c.endTime,
-                  priority: c.priority,
-                  location: c.location,
-                  tags: Array.isArray(c.tags) ? c.tags : [],
-                  isPublic: c.isPublic,
-                  assignees: Array.isArray(c.assignees) ? c.assignees : [],
-                  createdAt: c.createdAt,
-                  status: c.status || 'OPEN',
-                  author: c.author,
-                  opinions: c.opinions || {},
-                  opinions_inhabitants: c.opinions_inhabitants || []
-                });
-              }
+          for (const r of results) {
+            const { key, value: { content: c } } = r;
+            if (!c) continue;
+            if (c.type === 'tombstone') tombstoned.add(c.target);
+            if (c.type === 'task') {
+              if (c.replaces) replaced.set(c.replaces, key);
+              const status = c.status === 'OPEN' && moment(c.endTime).isBefore(now) ? 'CLOSED' : c.status;
+              tasks.set(key, { id: key, ...c, status });
             }
-            for (const replaced of replaces.keys()) {
-              byId.delete(replaced);
-            }
-            resolve(Array.from(byId.values()));
-          })
-        );
-      });
-    },
+          }
 
-    async createOpinion(id, category) {
-      const ssbClient = await openSsb();
-      const userId = ssbClient.id;
-      return new Promise((resolve, reject) => {
-        ssbClient.get(id, (err, msg) => {
-          if (err || !msg || msg.content?.type !== 'task') return reject(new Error('Task not found'));
-          if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'));
-          const tombstone = {
-            type: 'tombstone',
-            target: id,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          const updated = {
-            ...msg.content,
-            opinions: {
-              ...msg.content.opinions,
-              [category]: (msg.content.opinions?.[category] || 0) + 1
-            },
-            opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
-            updatedAt: new Date().toISOString(),
-            replaces: id
-          };
-          ssbClient.publish(tombstone, (err) => {
-            if (err) return reject(err);
-            ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
-          });
-        });
+          tombstoned.forEach(id => tasks.delete(id));
+          replaced.forEach((_, oldId) => tasks.delete(oldId));
+
+          resolve([...tasks.values()]);
+        }));
       });
     }
   };

+ 16 - 8
src/models/trending_model.js

@@ -8,7 +8,7 @@ module.exports = ({ cooler }) => {
   };
 
   const types = [
-    'bookmark', 'event', 'task', 'votes', 'report', 'feed',
+    'bookmark', 'votes', 'feed',
     'image', 'audio', 'video', 'document', 'transfer'
   ];
 
@@ -40,11 +40,14 @@ module.exports = ({ cooler }) => {
         tombstoned.add(c.target);
         continue;
       }
-      if (c.opinions) {
-        if (tombstoned.has(k)) continue;
-        if (c.replaces) replaces.set(c.replaces, k);
-        itemsById.set(k, m);
-      }
+	if (
+	  c.opinions &&
+	  !tombstoned.has(k) &&
+	  !['task', 'event', 'report'].includes(c.type)
+	) {
+	  if (c.replaces) replaces.set(c.replaces, k);
+	  itemsById.set(k, m);
+	}
     }
 
     for (const replacedId of replaces.keys()) {
@@ -105,8 +108,13 @@ module.exports = ({ cooler }) => {
     const msg = await getMessageById(contentId);
     if (!msg || !msg.content) throw new Error('Content not found');
 
-    const type = msg.content.type;
-    if (!types.includes(type)) throw new Error('Invalid content type for voting');
+	const type = msg.content.type;
+	if (
+	  !types.includes(type) ||
+	  ['task', 'event', 'report'].includes(type)
+	) {
+	  throw new Error('Voting not allowed on this content type');
+	}
 
     if (msg.content.opinions_inhabitants?.includes(userId)) throw new Error('Already voted');
 

+ 60 - 236
src/models/votes_model.js

@@ -10,8 +10,7 @@ module.exports = ({ cooler }) => {
       const ssb = await openSsb();
       const userId = ssb.id;
       const parsedDeadline = moment(deadline, moment.ISO_8601, true);
-      if (!parsedDeadline.isValid()) throw new Error('Invalid deadline');
-      if (parsedDeadline.isBefore(moment())) throw new Error('Deadline must be in the future');
+      if (!parsedDeadline.isValid() || parsedDeadline.isBefore(moment())) throw new Error('Invalid deadline');
       const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
       const content = {
         type: 'votes',
@@ -20,7 +19,7 @@ module.exports = ({ cooler }) => {
         deadline: parsedDeadline.toISOString(),
         createdBy: userId,
         status: 'OPEN',
-        votes: options.reduce((acc, opt) => { acc[opt] = 0; return acc; }, {}),
+        votes: options.reduce((acc, opt) => ({ ...acc, [opt]: 0 }), {}),
         totalVotes: 0,
         voters: [],
         tags,
@@ -28,266 +27,91 @@ module.exports = ({ cooler }) => {
         opinions_inhabitants: [],
         createdAt: new Date().toISOString()
       };
-      return new Promise((resolve, reject) => {
-        ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
-      });
-    },
-
-    async updateVoteById(id, updatedData) {
-      const ssb = await openSsb();
-      const userId = ssb.id;
-        const vote = await new Promise((resolve, reject) => {
-          ssb.get(id, (err, vote) => {
-          if (err || !vote?.content) return reject(new Error('Vote not found'));
-        resolve(vote);
-      });
-    });
-
-    if (vote.content.createdBy !== userId) {
-      throw new Error('Not the author');
-    }
-    if (vote.content.totalVotes > 0) {
-      throw new Error('Already voted');
-    }
-    const deadline = moment(vote.content.deadline);
-    if (!deadline.isValid() || deadline.isBefore(moment())) {
-      throw new Error('Deadline passed');
-    }
-    let tags = [];
-    if (updatedData.tags) {
-      if (Array.isArray(updatedData.tags)) {
-        tags = updatedData.tags.filter(Boolean); 
-      } else {
-        tags = updatedData.tags.split(',').map(t => t.trim()).filter(Boolean);
-      }
-    } else {
-      tags = vote.content.tags || [];
-    }
-
-    const tombstone = {
-      type: 'tombstone',
-      target: id,
-      deletedAt: new Date().toISOString(),
-      author: userId
-    };
-
-    const updated = {
-      ...vote.content,
-      ...updatedData,
-      tags,
-      updatedAt: new Date().toISOString(),
-      replaces: id
-    };
-    await new Promise((resolve, reject) => {
-      ssb.publish(tombstone, (err, res) => {
-        if (err) return reject(err);
-        resolve(res);
-      });
-    });
-    const result = await new Promise((resolve, reject) => {
-      ssb.publish(updated, (err, res) => {
-        if (err) return reject(err);
-        resolve(res);
-      });
-    });
-      return result;
+      return new Promise((res, rej) => ssb.publish(content, (err, msg) => err ? rej(err) : res(msg)));
     },
 
     async deleteVoteById(id) {
       const ssb = await openSsb();
       const userId = ssb.id;
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, vote) => {
-          if (err || !vote?.content) return reject(new Error('Vote not found'));
-          if (vote.content.createdBy !== userId) return reject(new Error('Not the author'));
-          const tombstone = {
-            type: 'tombstone',
-            target: id,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          ssb.publish(tombstone, (err, res) => err ? reject(err) : resolve(res));
-        });
-      });
+      const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
+      if (vote.content.createdBy !== userId) throw new Error('Not the author');
+      const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
+      return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result)));
     },
 
     async voteOnVote(id, choice) {
       const ssb = await openSsb();
       const userId = ssb.id;
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, vote) => {
-          if (err || !vote?.content) return reject(new Error('Vote not found'));
-          const validChoices = vote.content.options || [];
-          if (!validChoices.includes(choice)) return reject(new Error('Invalid choice'));
-          const { voters = [], votes = {}, totalVotes = 0 } = vote.content;
-          if (voters.includes(userId)) return reject(new Error('Already voted'));
-          votes[choice] = (votes[choice] || 0) + 1;
-          voters.push(userId);
-          const tombstone = {
-            type: 'tombstone',
-            target: id,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          const updated = {
-            ...vote.content,
-            votes,
-            voters,
-            totalVotes: totalVotes + 1,
-            updatedAt: new Date().toISOString(),
-            replaces: id
-          };
-          ssb.publish(tombstone, err => {
-            if (err) return reject(err);
-            ssb.publish(updated, (err2, res2) => err2 ? reject(err2) : resolve(res2));
-          });
-        });
-      });
+      const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
+      if (!vote.content.options.includes(choice)) throw new Error('Invalid choice');
+      if (vote.content.voters.includes(userId)) throw new Error('Already voted');
+      vote.content.votes[choice] += 1;
+      vote.content.voters.push(userId);
+      vote.content.totalVotes += 1;
+      const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
+      const updated = { ...vote.content, updatedAt: new Date().toISOString(), replaces: id };
+      await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
+      return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
     },
 
     async getVoteById(id) {
       const ssb = await openSsb();
-      return new Promise((resolve, reject) => {
-        ssb.get(id, async (err, vote) => {
-          if (err || !vote?.content) return reject(new Error('Vote not found'));
-          const c = vote.content;
-          const deadlineMoment = moment(c.deadline);
-          let status = c.status || 'OPEN';
-          if (deadlineMoment.isValid() && deadlineMoment.isBefore(moment()) && status !== 'CLOSED') {
-            const tombstone = {
-              type: 'tombstone',
-              target: id,
-              deletedAt: new Date().toISOString(),
-              author: c.createdBy
-            };
-            const updated = {
-              ...c,
-              status: 'CLOSED',
-              updatedAt: new Date().toISOString(),
-              replaces: id
-            };
-            await ssb.publish(tombstone);
-            await ssb.publish(updated);
-            status = 'CLOSED';
-          }
-          resolve({
-            id,
-            question: c.question,
-            options: c.options,
-            votes: c.votes,
-            totalVotes: c.totalVotes,
-            status,
-            deadline: c.deadline,
-            createdBy: c.createdBy,
-            createdAt: c.createdAt,
-            tags: c.tags || [],
-            opinions: c.opinions || {},
-            opinions_inhabitants: c.opinions_inhabitants || []
-          });
-        });
-      });
+      const now = moment();
+      const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
+      const c = vote.content;
+      const status = c.status === 'OPEN' && moment(c.deadline).isBefore(now) ? 'CLOSED' : c.status;
+      return { id, ...c, status };
     },
 
     async listAll(filter = 'all') {
       const ssb = await openSsb();
       const userId = ssb.id;
+      const now = moment();
       return new Promise((resolve, reject) => {
-        pull(
-          ssb.createLogStream(),
-          pull.collect(async (err, results) => {
-            if (err) return reject(err);
-            const tombstoned = new Set();
-            const replaces = new Map();
-            const byId = new Map();
-            const now = moment();
-            for (const r of results) {
-              const k = r.key;
-              const c = r.value.content;
-              if (!c) continue;
-              if (c.type === 'tombstone' && c.target) {
-                tombstoned.add(c.target);
-                continue;
-              }
-              if (c.type === 'votes') {
-                if (tombstoned.has(k)) continue;
-                if (c.replaces) replaces.set(c.replaces, k);
-                let status = c.status || 'OPEN';
-                const deadline = moment(c.deadline);
-                if (deadline.isValid() && deadline.isBefore(now) && status !== 'CLOSED') {
-                  const tomb = {
-                    type: 'tombstone',
-                    target: k,
-                    deletedAt: new Date().toISOString(),
-                    author: c.createdBy
-                  };
-                  const updated = {
-                    ...c,
-                    status: 'CLOSED',
-                    updatedAt: new Date().toISOString(),
-                    replaces: k
-                  };
-                  await ssb.publish(tomb);
-                  await ssb.publish(updated);
-                  status = 'CLOSED';
-                }
-                byId.set(k, {
-                  id: k,
-                  question: c.question,
-                  options: c.options,
-                  votes: c.votes,
-                  totalVotes: c.totalVotes,
-                  status,
-                  deadline: c.deadline,
-                  createdBy: c.createdBy,
-                  createdAt: c.createdAt,
-                  tags: c.tags || [],
-                  opinions: c.opinions || {},
-                  opinions_inhabitants: c.opinions_inhabitants || []
-                });
-              }
-            }
-            for (const replaced of replaces.keys()) {
-              byId.delete(replaced);
+        pull(ssb.createLogStream(), pull.collect((err, results) => {
+          if (err) return reject(err);
+          const tombstoned = new Set();
+          const replaced = new Map();
+          const votes = new Map();
+
+          for (const r of results) {
+            const { key, value: { content: c } } = r;
+            if (!c) continue;
+            if (c.type === 'tombstone') tombstoned.add(c.target);
+            if (c.type === 'votes') {
+              if (c.replaces) replaced.set(c.replaces, key);
+              const status = c.status === 'OPEN' && moment(c.deadline).isBefore(now) ? 'CLOSED' : c.status;
+              votes.set(key, { id: key, ...c, status });
             }
-            const out = Array.from(byId.values());
-            if (filter === 'mine') return resolve(out.filter(v => v.createdBy === userId));
-            if (filter === 'open') return resolve(out.filter(v => v.status === 'OPEN'));
-            if (filter === 'closed') return resolve(out.filter(v => v.status === 'CLOSED'));
-            resolve(out);
-          })
-        );
+          }
+
+          tombstoned.forEach(id => votes.delete(id));
+          replaced.forEach((_, oldId) => votes.delete(oldId));
+
+          const out = [...votes.values()];
+          if (filter === 'mine') return resolve(out.filter(v => v.createdBy === userId));
+          if (filter === 'open') return resolve(out.filter(v => v.status === 'OPEN'));
+          if (filter === 'closed') return resolve(out.filter(v => v.status === 'CLOSED'));
+          resolve(out);
+        }));
       });
     },
 
     async createOpinion(id, category) {
       const ssb = await openSsb();
       const userId = ssb.id;
-      return new Promise((resolve, reject) => {
-        ssb.get(id, (err, msg) => {
-          if (err || !msg || msg.content?.type !== 'votes') return reject(new Error('Vote not found'));
-          if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'));
-          const tombstone = {
-            type: 'tombstone',
-            target: id,
-            deletedAt: new Date().toISOString(),
-            author: userId
-          };
-          const updated = {
-            ...msg.content,
-            opinions: {
-              ...msg.content.opinions,
-              [category]: (msg.content.opinions?.[category] || 0) + 1
-            },
-            opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
-            updatedAt: new Date().toISOString(),
-            replaces: id
-          };
-          ssb.publish(tombstone, err => {
-            if (err) return reject(err);
-            ssb.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
-          });
-        });
-      });
+      const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
+      if (vote.content.opinions_inhabitants.includes(userId)) throw new Error('Already voted');
+      const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
+      const updated = {
+        ...vote.content,
+        opinions: { ...vote.content.opinions, [category]: (vote.content.opinions[category] || 0) + 1 },
+        opinions_inhabitants: [...vote.content.opinions_inhabitants, userId],
+        updatedAt: new Date().toISOString(),
+        replaces: id
+      };
+      await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
+      return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
     }
   };
 };

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

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

+ 1 - 1
src/server/package.json

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

+ 314 - 415
src/views/activity_view.js

@@ -14,6 +14,13 @@ function renderActionCards(actions) {
       if (content.type === 'tombstone') return false;
       if (content.type === 'post' && content.private === true) return false;
       if (content.type === 'tribe' && content.isAnonymous === true) return false;
+      if (content.type === 'task' && content.isPublic === "PRIVATE") return false;
+      if (content.type === 'event' && content.isPublic === "private") return false;
+      if (content.type === 'market') {
+        if (content.stock === 0 && content.status !== 'SOLD') {
+          return false; 
+        }
+      }
       return true;
     })
     .sort((a, b) => b.ts - a.ts);
@@ -32,493 +39,386 @@ function renderActionCards(actions) {
     const content = action.content || {};
     const cardBody = [];
 
-   if (type === 'votes') {
-    const { question, deadline, status, votes, totalVotes } = content;
-    const votesList = votes && typeof votes === 'object'
-      ? Object.entries(votes).map(([option, count]) => ({ option, count }))
-      : [];
-    cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/votes/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(`${question}`),
-        p(`${i18n.deadline}: ${deadline ? new Date(deadline).toLocaleString() : ''}`),
-        h2(`${i18n.voteTotalVotes}: ${totalVotes}`),
-        table(
+    if (type === 'votes') {
+      const { question, deadline, status, votes, totalVotes } = content;
+      const votesList = votes && typeof votes === 'object'
+        ? Object.entries(votes).map(([option, count]) => ({ option, count }))
+        : [];
+      cardBody.push(
+        div({ class: 'card-section votes' }, 
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.question + ':'), span({ class: 'card-value' }, question)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), span({ class: 'card-value' }, totalVotes)),
+          table(
             tr(...votesList.map(({ option }) => th(i18n[option] || option))),
             tr(...votesList.map(({ count }) => td(count)))
+          )
         )
-    );
-   }
-
-    if (type === 'transfer') {
-      const { from, to, concept, amount, deadline, status, tags, confirmedBy } = content;
-      const validTags = Array.isArray(tags) ? tags : [];
-      cardBody.push(
-         h2({ class: 'type-label' }, `[${typeLabel}]`),
-         form({ method: "GET", action: `/transfers/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-         ),
-         h2(i18n.concept + ": " + concept),
-         p(i18n.from + ": ", a({ href: `/author/${encodeURIComponent(from)}`, target: "_blank" }, from)),
-         p(i18n.to + ": ", a({ href: `/author/${encodeURIComponent(to)}`, target: "_blank" }, to)),
-         h2(i18n.amount + ": " + amount),
-         p(i18n.deadline + ": " + (deadline ? new Date(deadline).toLocaleString() : "")),
-         p(i18n.status + ": " + status),
-         p(`${i18n.transfersConfirmations}: ${confirmedBy.length}/2`),
-         validTags.length
-            ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-           : ""
       );
     }
-    
-    if (type === 'pixelia') {
-      const { author } = content;
+
+    if (type === 'transfer') {
+      const { from, to, concept, amount, deadline, status, confirmedBy } = content;
       cardBody.push(
-         h2({ class: 'type-label' }, `[${typeLabel}]`),
-         form({ method: "GET", action: `/pixelia` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-         ),
-         p(`${i18n.activityPixelia} ${i18n.pixeliaBy}: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)),
+        div({ class: 'card-section transfer' }, 
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)),
+          br,
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.from + ': '), a({ class: 'user-link', href: `/author/${encodeURIComponent(from)}`, target: "_blank" }, from)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ': '), a({ class: 'user-link', href: `/author/${encodeURIComponent(to)}`, target: "_blank" }, to)),
+          div({ class: 'card-field' }, h2({ class: 'card-label' }, i18n.transfersConfirmations + ': ' + `${confirmedBy.length}/2`)),
+        )
       );
     }
 
+	if (type === 'pixelia') {
+	  const { author } = content;
+	  cardBody.push(
+	    div({ class: 'card-section pixelia' },
+	      div({ class: 'card-field' },
+		a({ href: `/author/${encodeURIComponent(author)}`, class: 'activityVotePost' }, author)
+	      )
+	    )
+	  );
+	}
+
     if (type === 'tribe') {
-      const { title, description, image, location, tags, isLARP, isAnonymous, members, createdAt, author } = content;
+      const { title, image, description, tags, isLARP, inviteMode, isAnonymous, members } = content;
       const validTags = Array.isArray(tags) ? tags : [];
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/tribe/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(`${title}`),
-        p(`${description || ''}`),
-	image 
-	  ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image' }) 
-	  : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }),
-        p(`${i18n.location || 'Location'}: ${location || ''}`),
-        typeof isLARP === 'boolean' ? p(`LARP: ${isLARP ? 'Yes' : 'No'}`) : "",
-        typeof isAnonymous === 'boolean' ? p(`Anonymous: ${isAnonymous ? 'Yes' : 'No'}`) : "",
-        Array.isArray(members) ? h2(`${i18n.tribeMembersCount || 'Members'}: ${members.length}`) : "",
-        createdAt ? p(`${i18n.createdAt}: ${new Date(createdAt).toLocaleString()}`) : "",
-        author ? p(`${i18n.author}: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)) : "",
-        validTags.length
-            ? div(validTags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-              ))
+        div({ class: 'card-section tribe' },
+	h2({ class: 'tribe-title' }, 
+	  a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title)
+	),
+        p({ class: 'tribe-description' }, description || ''),
+          image
+            ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image tribe-image' })
+            : img({ src: '/assets/images/default-tribe.png', class: 'feed-image tribe-image' }),
+          br(),
+          typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "",      
+          inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "",
+          typeof isLARP === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel+ ':'), span({ class: 'card-value' }, isLARP ? i18n.tribeYes : i18n.tribeNo)) : "",
+          Array.isArray(members) ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeMembersCount) + ':'), span({ class: 'card-value' }, members.length)) : "",
+          validTags.length
+            ? div({ class: 'card-tags' }, validTags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
             : ""
+        )
       );
     }
 
     if (type === 'curriculum') {
-      const { author, name, description, photo, personalSkills, personalExperiences, oasisExperiences, oasisSkills, educationExperiences, educationalSkills, languages, professionalExperiences, professionalSkills, location, status,  preferences, createdAt} = content;
+      const { author, name, description, photo, personalSkills, oasisSkills, educationalSkills, languages, professionalSkills, status, preferences} = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/inhabitant/${encodeURIComponent(action.author)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(`${name}`),
-        description ? p(`${i18n.description}: ${description}`) : "",
-        photo ? img({ src: `/blob/${encodeURIComponent(photo)}`, class: 'feed-image' }) : "",
-        location ? p(`${i18n.cvLocationLabel || 'Location'}: ${location}`) : "",
-        languages ? p(`${i18n.cvLanguagesLabel || 'Languages'}: ${languages}`) : "",
-        createdAt ? p(`${i18n.cvCreatedAt}: ${new Date(createdAt).toLocaleString()}`) : "".
-        br,
-        personalExperiences ? p(`${i18n.cvPersonalExperiencesLabel || 'Personal Experiences'}: ${personalExperiences}`) : "",
-        personalSkills && personalSkills.length
-            ? div(personalSkills.map(skill =>
-                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
-            )) : "",
-        oasisExperiences ? p(`${i18n.cvOasisExperiencesLabel || 'Oasis Experiences'}: ${oasisExperiences}`) : "",
-        oasisSkills && oasisSkills.length
-            ? div(oasisSkills.map(skill =>
-                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
-            )) : "",
-        educationExperiences ? p(`${i18n.cvEducationExperiencesLabel || 'Education Experiences'}: ${educationExperiences}`) : "",
-        educationalSkills && educationalSkills.length
-            ? div(educationalSkills.map(skill =>
-                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
-            )) : "",
-        professionalExperiences ? p(`${i18n.cvProfessionalExperiencesLabel || 'Professional Experiences'}: ${professionalExperiences}`) : "",
-        professionalSkills && professionalSkills.length
-            ? div(professionalSkills.map(skill =>
-                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
-            )) : "",
-        status ? h2(`${i18n.cvStatusLabel}: ${status}`) : "",
-        preferences ? p(`${i18n.cvPreferencesLabel || 'Preferences'}: ${preferences}`) : "",
-        h2(`${i18n.activityContact}: `, a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author))
+        div({ class: 'card-section curriculum' },
+          h2(a({ href: `/author/${encodeURIComponent(author)}`, class: "user-link" }, `@`, name)),
+          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, status)) : "",
+          preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.cvPreferencesLabel || 'Preferences') + ':'), span({ class: 'card-value' }, preferences)) : "",
+          languages ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.cvLanguagesLabel || 'Languages') + ':'), span({ class: 'card-value' }, languages)) : "",
+          br(),
+          photo ? img({ class: "cv-photo", src: `/blob/${encodeURIComponent(photo)}` }) : "",
+          br(),
+          description ? div({ class: 'card-field' }, span({ class: 'card-value' }, description)) : "",
+          br(),
+	  personalSkills && personalSkills.length
+	  ? div({ class: 'card-tags' }, personalSkills.map(skill =>
+	      a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
+	  )) : "",
+	  oasisSkills && oasisSkills.length
+	  ? div({ class: 'card-tags' }, oasisSkills.map(skill =>
+	      a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
+	  )) : "",
+	  educationalSkills && educationalSkills.length
+	  ? div({ class: 'card-tags' }, educationalSkills.map(skill =>
+	      a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
+	  )) : "",
+	  professionalSkills && professionalSkills.length
+	  ? div({ class: 'card-tags' }, professionalSkills.map(skill =>
+	      a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
+	  )) : "",
+        )
       );
     }
 
     if (type === 'image') {
-      const { url, title, description, tags, meme } = content;
-      const validTags = Array.isArray(tags) ? tags : [];
-        cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/images/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        title ? h2(title) : "",
-        description ? p(description) : "",
-        meme ? h2(`${i18n.trendingCategory}: ${i18n.meme}`) : "",
-        img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image' }),
-        br,
-        validTags.length
-           ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-           : ""
+      const { url } = content;
+      cardBody.push(
+        div({ class: 'card-section image' },    
+          img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image img-content' })
+        )
       );
     }
 
     if (content.type === 'audio') {
-      const { url, mimeType, title, description, tags } = content;
-      const validTags = Array.isArray(tags) ? tags : [];
+      const { url, mimeType, title } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/audios/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        title?.trim() ? h2(title) : "",
-        description?.trim() ? p(description) : "",
-        url
-          ? div({ class: "audio-container" },
-              audioHyperaxe({
-                controls: true,
-                src: `/blob/${encodeURIComponent(url)}`,
-                type: mimeType
-              })
-            )
-          : p(i18n.audioNoFile),
-        validTags.length
-           ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-           : ""
+        div({ class: 'card-section audio' }, 
+          title?.trim() ? h2({ class: 'audio-title' }, title) : "",
+          url
+            ? div({ class: "audio-container" },
+                audioHyperaxe({
+                  controls: true,
+                  src: `/blob/${encodeURIComponent(url)}`,
+                  type: mimeType
+                })
+              )
+            : p(i18n.audioNoFile),
+        )
       );
     }
 
     if (content.type === 'video') {
-      const { url, mimeType, title, description, tags } = content;
-      const validTags = Array.isArray(tags) ? tags : [];
+      const { url, mimeType, title } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/videos/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        title?.trim() ? h2(title) : "",
-        description?.trim() ? p(description) : "",
-        url
-          ? div({ class: "video-container" },
-              videoHyperaxe({
-                controls: true,
-                src: `/blob/${encodeURIComponent(url)}`,
-                type: mimeType,
-                preload: 'metadata',
-                width: '640',
-                height: '360'
-              })
-            )
-          : p(i18n.videoNoFile),
-        validTags.length
-           ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-           : ""
+        div({ class: 'card-section video' },     
+          title?.trim() ? h2({ class: 'video-title' }, title) : "",
+          url
+            ? div({ class: "video-container" },
+                videoHyperaxe({
+                  controls: true,
+                  src: `/blob/${encodeURIComponent(url)}`,
+                  type: mimeType,
+                  preload: 'metadata',
+                  width: '640',
+                  height: '360'
+                })
+              )
+            : p(i18n.videoNoFile)
+        )
       );
     }
 
     if (content.type === 'document') {
-       const { url, title, description, tags, key } = content;
-       const validTags = Array.isArray(tags) ? tags : [];
-       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/documents/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        title?.trim() ? h2(title) : "",
-        description?.trim() ? p(description) : "",
-        div({
-          id: `pdf-container-${key || url}`,
-          class: 'pdf-viewer-container',
-          'data-pdf-url': `/blob/${encodeURIComponent(url)}`
-        }),
-        tags.length
-          ? div(validTags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-            ))
-          : null
+      const { url, title, key } = content;
+      cardBody.push(
+        div({ class: 'card-section document' },      
+          title?.trim() ? h2({ class: 'document-title' }, title) : "",
+          div({
+            id: `pdf-container-${key || url}`,
+            class: 'pdf-viewer-container',
+            'data-pdf-url': `/blob/${encodeURIComponent(url)}`
+          })
+        )
       );
     }
 
     if (type === 'bookmark') {
-      const { author, url, tags, description, category, lastVisit } = content;
-      const validTags = Array.isArray(tags) ? tags : [];
+      const { url, description, lastVisit } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/bookmarks/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        description ? p(`${description}`) : "",  
-        h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : ""),
-        category ? p(`${i18n.bookmarkCategory}: ${category}`) : "",
-        lastVisit ? p(`${i18n.bookmarkLastVisit}: ${new Date(lastVisit).toLocaleString()}`) : "",
-        validTags.length
-            ? div(validTags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-              ))
-            : ""
-        );
+        div({ class: 'card-section bookmark' },       
+          description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ':'), span({ class: 'card-value' }, description)) : "",
+          h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : ""),
+          lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(lastVisit).toLocaleString())) : ""
+        )
+      );
     }
 
     if (type === 'event') {
-        const { title, description, date, location, price, url: eventUrl, attendees, tags, organizer, status, isPublic } = content;
-        const validTags = Array.isArray(tags) ? tags : [];
-        cardBody.push(
-          h2({ class: 'type-label' }, `[${typeLabel}]`),
-          form({ method: "GET", action: `/events/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-          ),
-            p(`${i18n.title}: ${title}`),
-            description ? p(`${i18n.description}: ${description}`) : "",
-            date ? p(`${i18n.date}: ${new Date(date).toLocaleString()}`) : "",
-            location ? p(`${i18n.location || 'Location'}: ${location}`) : "",
-            status ? p(`${i18n.status}: ${status}`) : "",
-            typeof isPublic === 'boolean' ? p(`${i18n.isPublic || 'Public'}: ${isPublic ? 'Yes' : 'No'}`) : "",
-            price ? p(`${i18n.price || 'Price'}: ${price} ECO`) : "",
-            eventUrl ? p(`${i18n.trendingUrl}: `, a({ href: eventUrl, target: '_blank' }, eventUrl)) : "",
-            organizer ? p(`${i18n.organizer || 'Organizer'}: `, a({ href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
-            Array.isArray(attendees) ? p(`${i18n.attendees}: ${attendees.length}`) : "",
-            validTags.length
-            ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-            : "",
-        );
+      const { title, description, date, location, price, url: eventUrl, attendees, organizer, status, isPublic } = content;
+      cardBody.push(
+        div({ class: 'card-section event' },    
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
+          description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.description + ':'), span({ class: 'card-value' }, description)) : "",
+          date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, new Date(date).toLocaleString())) : "",
+          location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "",
+          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "",
+          typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
+          price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
+          eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingUrl + ':'), a({ href: eventUrl, target: '_blank' }, eventUrl)) : "",
+          br,
+          organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), a({ class: "user-link", href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
+          Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : "",   
+        )
+      );
     }
 
     if (type === 'task') {
-        const { title, description, startTime, endTime, priority, location, tags, isPublic, assignees, status, author } = content;
-        const validTags = Array.isArray(tags) ? tags : [];
-        cardBody.push(
-          h2({ class: 'type-label' }, `[${typeLabel}]`),
-          form({ method: "GET", action: `/tasks/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-          ),
-            p(`${i18n.title}: ${title}`),
-            description ? p(`${i18n.description}: ${description}`) : "",
-            startTime ? p(`${i18n.startTime || 'Start'}: ${new Date(startTime).toLocaleString()}`) : "",
-            endTime ? p(`${i18n.endTime || 'End'}: ${new Date(endTime).toLocaleString()}`) : "",
-            priority ? p(`${i18n.priority || 'Priority'}: ${priority}`) : "",
-            location ? p(`${i18n.location || 'Location'}: ${location}`) : "",
-            validTags.length
-             ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-            : "",
-            typeof isPublic === 'boolean' ? p(`${i18n.isPublic || 'Public'}: ${isPublic ? 'Yes' : 'No'}`) : "",
-            Array.isArray(assignees) ? p(`${i18n.taskAssignees || 'Assignees'}: ${assignees.length}`) : "",
-            status ? p(`${i18n.status}: ${status}`) : "",
-            author ? p(`${i18n.author || 'Author'}: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)) : ""
-        );
+      const { title, startTime, endTime, priority, status, author } = content;
+      cardBody.push(
+        div({ class: 'card-section task' },
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
+          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "",
+          priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.priority || 'Priority') + ':'), span({ class: 'card-value' }, priority)) : "",
+          startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, new Date(startTime).toLocaleString())) : "",
+          endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, new Date(endTime).toLocaleString())) : "",
+        )
+      );
     }
-    
+
     if (type === 'feed') {
-      const { text, author, createdAt, opinions, opinions_inhabitants, refeeds, refeeds_inhabitants } = content;
+      const { text, refeeds } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        h2(text),
-        p(i18n.author + ": ", a({ href: `/author/${encodeURIComponent(author)}`, target: "_blank" }, author)),
-        p(i18n.createdAt + ": " + new Date(createdAt).toLocaleString()),
-        h2(i18n.tribeFeedRefeeds + ": " + refeeds)
+        div({ class: 'card-section feed' }, 
+          h2({ class: 'feed-title' }, text),
+          h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-label' }, refeeds))
+        )
       );
     }
-    
+
     if (type === 'post') {
       const { contentWarning, text } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/thread/${encodeURIComponent(action.id)}#${encodeURIComponent(action.id)}` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        contentWarning ? h2(contentWarning) : '',
-        p({ innerHTML: text })
-      );
-   }
-   
-   if (type === 'vote') {
-     const { vote } = content;
-     cardBody.push(
-       h2({ class: 'type-label' }, `[${typeLabel}]`),
-       p(
-         a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author),
-         ` ${i18n.activitySpread} `,
-         a({ href: `/thread/${encodeURIComponent(vote.link)}#${encodeURIComponent(vote.link)}` }, vote.link)
-       )
-     );
-   }
-   
-   if (type === 'about') {
-     const { about, name, description } = content;
-     cardBody.push(
-       h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/author/${encodeURIComponent(action.author)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-         ),
-         h2(a({ href: `/author/${encodeURIComponent(about)}` },`@`,name)),
-         p(description)
-     );
-   }  
-   
-    if (type === 'contact') {
-      const { contact } = content;
-      cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/inhabitants` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-      p(
-      `${i18n.activitySupport}: `,
-      a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author),
-      span({ class: 'action-meta' }, " <-> "),
-      a({ href: `/author/${encodeURIComponent(contact)}` }, contact)
-      )
+        div({ class: 'card-section post' },
+          contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
+          p({ innerHTML: text }) 
+        )
       );
     }
-    
-    if (type === 'pub') {
-      const { address } = content;
-      const { host, key } = address;
+
+    if (type === 'vote') {
+      const { vote } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-          form({ method: "GET", action: `/invites` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-	p(
-	  ` ${i18n.activityJoin}: `,
-	  a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author),
-          span({ class: 'action-meta' }, " -> "),
-	  a({ href: `/author/${encodeURIComponent(key)}` }, key),
-	  ` (`,
-	  host,
-	  `)`
-	)
+        div({ class: 'card-section vote' },
+           p(
+        	a({ href: `/thread/${encodeURIComponent(vote.link)}#${encodeURIComponent(vote.link)}`, class: 'activityVotePost' }, vote.link)
+	      )
+	    )
+	  );
+	}
+
+    if (type === 'about') {
+      const { about, name, description, image } = content;
+      cardBody.push(
+        div({ class: 'card-section about' },
+        h2(a({ href: `/author/${encodeURIComponent(about)}`, class: "user-link" }, `@`, name)),
+          image
+            ? img({ src: `/blob/${encodeURIComponent(image)}` })
+            : img({ src: '/assets/images/default-avatar.png', alt: name })
+        )
       );
     }
-    
+
+	if (type === 'contact') {
+	  const { contact } = content;
+	  cardBody.push(
+	    div({ class: 'card-section contact' },
+	      p({ class: 'card-field' }, 
+		a({ href: `/author/${encodeURIComponent(contact)}`, class: 'activitySpreadInhabitant2' }, contact)
+	      )
+	    )
+	  );
+	}
+
+	if (type === 'pub') {
+	  const { address } = content;
+	  const { host, key } = address;
+	  cardBody.push(
+	    div({ class: 'card-section pub' },
+	      p({ class: 'card-field' },
+		a({ href: `/author/${encodeURIComponent(key)}`, class: 'activitySpreadInhabitant2' }, key)
+	      )
+	    )
+	  );
+	}
+
     if (type === 'market') {
-      const { item_type, title, description, price, tags, status, item_status, deadline, includesShipping, seller, image, auctions_poll } = content;
-      const validTags = Array.isArray(tags) ? tags : [];
+      const { item_type, title, price, status, deadline, stock, image, auctions_poll } = content;
       cardBody.push(
-        h2({ class: 'type-label' }, `[${typeLabel}]`),
-        form({ method: "GET", action: `/market/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        p(i18n.marketItemTitle + ": " + title),
-        p(i18n.marketItemDescription + ": " + description),
-        image
+        div({ class: 'card-section market' }, 
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemTitle + ':'), span({ class: 'card-value' }, title)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, item_type.toUpperCase())),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")),
+          br,
+          image
             ? img({ src: `/blob/${encodeURIComponent(image)}` })
             : img({ src: '/assets/images/default-market.png', alt: title }),
-        p(i18n.marketItemType + ": " + item_type),
-        p(i18n.marketItemCondition + ": " + item_status),
-        p(i18n.marketItemIncludesShipping + ": " + (includesShipping ? i18n.YESLabel : i18n.NOLabel)),
-        p(i18n.deadline + ": " + (deadline ? new Date(deadline).toLocaleString() : "")),
-        p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(seller)}` }, seller)),
-        validTags.length
-           ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-           : "",
-        h2(i18n.marketItemStatus + ": " + status),
-        div({ class: "market-card price" },
-          p(`${i18n.marketItemPrice}: ${price} ECO`)
-        ),
-    );
-    if (item_type === 'auction') {
-        if (status !== 'SOLD' && status !== 'DISCARDED') {
-            cardBody.push(
-                div({ class: "auction-info" },
-                    auctions_poll && auctions_poll.length > 0
-                        ? [
-                            p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
-                            table({ class: 'auction-bid-table' },
-                                tr(
-                                    th(i18n.marketAuctionBidTime),
-                                    th(i18n.marketAuctionUser),
-                                    th(i18n.marketAuctionBidAmount)
-                                ),
-                                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`)
-                                    );
-                                })
-                            )
-                        ]
-                        : p(i18n.marketNoBids),
-                    form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
-                        input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
-                        br(),
-                        button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
-                    )
-                )
-            );
-        }
-    }
-    if (item_type === 'exchange') {
-        if (status !== 'SOLD' && status !== 'DISCARDED') {
-            cardBody.push(
-                form({ method: "POST", action: `/market/buy/${encodeURIComponent(action.id)}` },
-                    button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
+          br,
+          div({ class: "market-card price" },
+            p(`${i18n.marketItemPrice}: ${price} ECO`)
+          ),
+          item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED'
+            ? div({ class: "auction-info" },
+                auctions_poll && auctions_poll.length > 0
+                  ? [
+                      p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
+                      table({ class: 'auction-bid-table' },
+                        tr(
+                          th(i18n.marketAuctionBidTime),
+                          th(i18n.marketAuctionUser),
+                          th(i18n.marketAuctionBidAmount)
+                        ),
+                        ...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`)
+                          );
+                        })
+                      )
+                    ]
+                  : p(i18n.marketNoBids),
+                form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
+                  input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
+                  br(),
+                  button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
                 )
-            );
-        }
+              ) : "",
+          item_type === 'exchange' && status !== 'SOLD' && status !== 'DISCARDED'
+            ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(action.id)}` },
+                button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
+              ) : ""
+        )
+      );
     }
-  }
 
     if (type === 'report') {
-        const { title, description, category, createdAt, author, image, tags, confirmations, severity, status, isAnonymous } = content;
-        const validTags = Array.isArray(tags) ? tags : [];
-        cardBody.push(
-          h2({ class: 'type-label' }, `[${typeLabel}]`),
-          form({ method: "GET", action: `/reports/${encodeURIComponent(action.id)}` },
-            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-          ),
-            p(`${i18n.title}: ${title}`),
-            description ? p(`${i18n.description}: ${description}`) : "",
-            category ? p(`${i18n.category}: ${category}`) : "",
-            severity ? p(`${i18n.severity || 'Severity'}: ${severity}`) : "",
-            status ? p(`${i18n.status}: ${status}`) : "",
-            image ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image' }) : "",
-            createdAt ? p(`${i18n.reportsCreatedAt}: ${new Date(createdAt).toLocaleString()}`) : "",    
-	    p(`${i18n.author || 'Author'}: `, 
-		  typeof isAnonymous === 'boolean' 
-		    ? (isAnonymous 
-			? i18n.reportsAnonymousAuthor || 'Anonymous' 
-			: a({ href: `/author/${encodeURIComponent(author)}`, target: '_blank' }, author)) 
-		    : author ? a({ href: `/author/${encodeURIComponent(author)}`, target: '_blank' }, author) : ""
-		),
-            Array.isArray(confirmations) ? h2(`${i18n.confirmations || 'Confirmations'}: ${confirmations.length}`) : "",
-            validTags.length
-             ? div(validTags.map(tag =>
-               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-             ))
-             : ""
-        );
+      const { title, confirmations, severity, status } = content;
+      cardBody.push(
+        div({ class: 'card-section report' },      
+          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
+          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "",
+          severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.severity || 'Severity') + ':'), span({ class: 'card-value' }, severity.toUpperCase())) : "",
+          Array.isArray(confirmations) ? h2({ class: 'card-label' }, (i18n.transfersConfirmations) + ': ' + confirmations.length) : "",   
+        )
+      );
     }
 
-    return div({ class: 'action-card' },
-      p({ class: 'action-meta' }, `${date} ${i18n.performed} `, userLink),
-      ...cardBody
-    );
+return div({ class: 'card card-rpg' },
+  div({ class: 'card-header' },
+    h2({ class: 'card-label' }, `[${typeLabel}]`),
+    type !== 'feed' ? form({ method: "GET", action: getViewDetailsAction(type, action) },
+      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+    ) : ''
+  ),
+  div({ class: 'card-body' }, ...cardBody),
+  p({ class: 'card-footer' },
+    span({ class: 'date-link' }, `${date} ${i18n.performed} `),
+    a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
+  )
+);
+
   });
 }
 
+function getViewDetailsAction(type, action) {
+  switch (type) {
+    case 'votes': return `/votes/${encodeURIComponent(action.id)}`;
+    case 'transfer': return `/transfers/${encodeURIComponent(action.id)}`;
+    case 'pixelia': return `/pixelia`;
+    case 'tribe': return `/tribe/${encodeURIComponent(action.id)}`;
+    case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
+    case 'image': return `/images/${encodeURIComponent(action.id)}`;
+    case 'audio': return `/audios/${encodeURIComponent(action.id)}`;
+    case 'video': return `/videos/${encodeURIComponent(action.id)}`;
+    case 'document': return `/documents/${encodeURIComponent(action.id)}`;
+    case 'bookmark': return `/bookmarks/${encodeURIComponent(action.id)}`;
+    case 'event': return `/events/${encodeURIComponent(action.id)}`;
+    case 'task': return `/tasks/${encodeURIComponent(action.id)}`;    
+    case 'about': return `/author/${encodeURIComponent(action.author)}`;
+    case 'post': return `/thread/${encodeURIComponent(action.id)}#${encodeURIComponent(action.id)}`;
+    case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
+    case 'contact': return `/inhabitants`;
+    case 'pub': return `/invites`;
+    case 'market': return `/market/${encodeURIComponent(action.id)}`;
+    case 'report': return `/reports/${encodeURIComponent(action.id)}`;
+    }
+   }
+
 exports.activityView = (actions, filter, userId) => {
   const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity;
   const desc = i18n.activityDesc;
@@ -630,4 +530,3 @@ exports.activityView = (actions, filter, userId) => {
   }
   return html;
 };
-

+ 149 - 118
src/views/agenda_view.js

@@ -1,13 +1,159 @@
-const { div, h2, p, section, button, form, img, textarea, a, br, h1 } = require("../server/node_modules/hyperaxe");
+const { div, h2, p, section, button, form, img, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const moment = require('../server/node_modules/moment');
 const { config } = require('../server/SSB_server.js');
 
 userId = config.keys.id;
 
+const renderCardField = (labelText, value) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, value)
+  );
+  
+function getViewDetailsAction(item) {
+  switch (item.type) {
+    case 'transfer': return `/transfers/${encodeURIComponent(item.id)}`;
+    case 'tribe': return `/tribe/${encodeURIComponent(item.id)}`;
+    case 'event': return `/events/${encodeURIComponent(item.id)}`;
+    case 'task': return `/tasks/${encodeURIComponent(item.id)}`;    
+    case 'market': return `/market/${encodeURIComponent(item.id)}`;
+    case 'report': return `/reports/${encodeURIComponent(item.id)}`;
+    }
+   }
+
+const renderAgendaItem = (item, userId, filter) => {
+  const fmt = d => moment(d).format('YYYY/MM/DD HH:mm:ss');
+  const author = item.seller || item.organizer || item.from || item.author;
+
+  const commonFields = [
+    p({ class: 'card-footer' },
+      span({ class: 'date-link' }, `${moment(item.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+      a({ href: `/author/${encodeURIComponent(author)}`, class: 'user-link' }, `${author}`)
+    )
+  ];
+  let details = [];
+  let actionButton = null;
+  if (filter === 'discarded') {
+    actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}` },
+      button({ type: 'submit', class: 'restore-btn' }, i18n.agendaRestoreButton)
+    );
+  } else {
+    actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}` },
+      button({ type: 'submit', class: 'discard-btn' }, i18n.agendaDiscardButton)
+    );
+  }
+  if (item.type === 'market') {
+  details = [
+    renderCardField(i18n.marketItemDescription + ":", item.description), 
+    renderCardField(i18n.marketItemType + ":", item.item_type),
+    renderCardField(i18n.marketItemStatus + ":", item.status),
+    renderCardField(i18n.marketItemPrice + ":", `${item.price} ECO`),
+    renderCardField(i18n.marketItemIncludesShipping + ":", item.includesShipping ? i18n.agendaYes : i18n.agendaNo),
+    renderCardField(i18n.marketItemStock + ":", item.stock), 
+    renderCardField(i18n.deadline + ":", new Date(item.deadline).toLocaleString()),
+   ];
+    if (item.item_type === 'auction') {
+      const bids = item.auctions_poll.map(bid => parseFloat(bid.split(':')[1]))
+      const maxBid = bids.length ? Math.max(...bids) : 0;
+          details.push(
+      renderCardField(i18n.marketItemHighestBid + ":", `${maxBid} ECO`));
+    };
+    const seller = p(a({ class: "user-link", href: `/author/${encodeURIComponent(item.seller)}` }, item.seller))
+      details.push(
+      br,
+      div({ class: 'members-list' }, i18n.marketItemSeller + ': ', seller)
+    );
+  }
+
+  if (item.type === 'tribe') {
+    details = [
+      renderCardField(i18n.agendaDescriptionLabel + ":",item.description || i18n.noDescription),
+      renderCardField(i18n.agendaAnonymousLabel + ":", item.isAnonymous ? i18n.agendaYes : i18n.agendaNo),
+      renderCardField(i18n.agendaInviteModeLabel + ":", item.inviteMode || i18n.noInviteMode),
+      renderCardField(i18n.agendaLARPLabel + ":", item.isLARP ? i18n.agendaYes : i18n.agendaNo),
+      renderCardField(i18n.agendaLocationLabel + ":", item.location || i18n.noLocation),
+      renderCardField(i18n.agendaMembersCount + ":", item.members.length || 0),
+      br(),
+    ];
+    const membersList = item.members.map(member =>
+      p(a({ class: "user-link", href: `/author/${encodeURIComponent(member)}` }, member))
+    );
+    details.push(
+      div({ class: 'members-list' }, `${i18n.agendaMembersLabel}:`, membersList)
+    );
+  }
+
+  if (item.type === 'report') {
+    details = [
+      renderCardField(i18n.agendareportDescription + ":", item.description || i18n.noDescription),
+      renderCardField(i18n.agendareportStatus + ":", item.status || i18n.noStatus),
+      renderCardField(i18n.agendareportCategory + ":", item.category || i18n.noCategory),
+      renderCardField(i18n.agendareportSeverity + ":", item.severity || i18n.noSeverity),
+    ];
+  }
+
+  if (item.type === 'event') {
+    details = [
+      renderCardField(i18n.eventDescriptionLabel + ":", item.description),
+      renderCardField(i18n.eventDateLabel + ":", fmt(item.date)),
+      renderCardField(i18n.eventLocationLabel + ":", item.location),
+      renderCardField(i18n.eventPriceLabel + ":", `${item.price} ECO`),
+      renderCardField(i18n.eventUrlLabel + ":", item.url || i18n.noUrl)
+    ];
+
+    actionButton = actionButton || form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` },
+      button({ type: 'submit', class: 'assign-btn' }, `${i18n.eventAttendButton}`)
+    );
+  }
+
+  if (item.type === 'task') {
+    details = [
+      renderCardField(i18n.taskDescriptionLabel + ":", item.description),
+      renderCardField(i18n.taskStatus + ":", item.status),
+      renderCardField(i18n.taskPriorityLabel + ":", item.priority),
+      renderCardField(i18n.taskStartTimeLabel + ":",  new Date(item.startTime).toLocaleString()),
+      renderCardField(i18n.taskEndTimeLabel + ":", new Date(item.endTime).toLocaleString()),
+      renderCardField(i18n.taskLocationLabel + ":", item.location),
+
+    ];
+
+    const assigned = Array.isArray(item.assignees) && item.assignees.includes(userId);
+    actionButton = actionButton || form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` },
+      button({ type: 'submit', class: 'assign-btn' },
+        assigned ? i18n.taskUnassignButton : i18n.taskAssignButton
+      )
+    );
+  }
+
+  if (item.type === 'transfer') {
+    details = [
+      renderCardField(i18n.agendaTransferConcept + ":", item.concept),
+      renderCardField(i18n.agendaTransferAmount + ":", item.amount),
+      renderCardField(i18n.agendaTransferDeadline + ":", fmt(item.deadline)),
+      br,
+    ];
+    const membersList = p(a({ class: "user-link", href: `/author/${encodeURIComponent(item.to)}` }, item.to))
+    details.push(
+      div({ class: 'members-list' }, i18n.to + ': ', membersList)
+    );
+  }
+
+  return div({ class: 'agenda-item card' },
+    h2(`[${item.type.toUpperCase()}] ${item.title || item.name || item.concept}`),
+    form({ method: "GET", action: getViewDetailsAction(item) },
+      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+    ),    
+    actionButton,
+    br,
+    ...details,
+    br,
+    ...commonFields,
+  );
+};
+
 exports.agendaView = async (data, filter) => {
   const { items, counts } = data;
-  const fmt = d => moment(d).format('YYYY/MM/DD HH:mm:ss');
 
   return template(
     i18n.agendaTitle,
@@ -42,122 +188,7 @@ exports.agendaView = async (data, filter) => {
       ),
       div({ class: 'agenda-list' },
         items.length
-          ? items.map(item => {
-              const author = item.seller || item.organizer || item.from || item.author;
-              const commonFields = [
-                p(`${i18n.agendaAuthor}: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)),
-                p(`${i18n.agendaCreatedAt}: ${fmt(item.createdAt)}`)
-              ];
-              let details = [];
-              let actionButton = null;
-              if (filter === 'discarded') {
-		actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}` },
-		  button({ type: 'submit', class: 'restore-btn' }, i18n.agendaRestoreButton)
-		);
-              } else {
-		actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}` },
-		  button({ type: 'submit', class: 'discard-btn' }, i18n.agendaDiscardButton)
-		);
-              }
-
-              if (item.type === 'market') {
-                commonFields.push(p(`${i18n.marketItemType}: ${item.item_type}`));
-                commonFields.push(p(`${i18n.marketItemTitle}: ${item.title}`));
-                commonFields.push(p(`${i18n.marketItemDescription}: ${item.description}`));
-                commonFields.push(p(`${i18n.marketItemPrice}: ${item.price} ECO`));
-                commonFields.push(p(`${i18n.marketItemIncludesShipping}: ${item.includesShipping ? i18n.agendaYes : i18n.agendaNo}`));
-                commonFields.push(p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)));
-                commonFields.push(p(`${i18n.marketItemAvailable}: ${moment(item.createdAt).format('YYYY-MM-DD HH:mm')}`));
-                commonFields.push(
-                  item.image
-                    ? img({ src: `/blob/${encodeURIComponent(item.image)}`, class: 'market-image' })
-                    : p(i18n.marketNoImage)
-                );
-                commonFields.push(
-                  item.tags && item.tags.length
-                    ? div(
-                        item.tags.map(tag =>
-                          a(
-                            {
-                              href: `/search?query=%23${encodeURIComponent(tag)}`,
-                              class: 'tag-link',
-                              style: 'margin-right:0.8em;',
-                            },
-                            `#${tag}`
-                          )
-                        )
-                      )
-                    : null
-                );
-                if (item.item_type === 'auction') {
-                  details.push(p(`${i18n.marketItemAvailable}: ${moment(item.deadline).format('YYYY-MM-DD HH:mm')}`));
-                  const bids = item.auctions_poll.map(bid => parseFloat(bid.split(':')[1]));
-                  const maxBid = bids.length ? Math.max(...bids) : 0;
-                  details.push(p(`${i18n.marketItemHighestBid}: ${maxBid} ECO`));
-                }
-                details.push(p(`${i18n.marketItemStatus}: ${item.status}`));
-              }
-              if (item.type === 'tribe') {
-                commonFields.push(p(`${i18n.agendaDescriptionLabel}: ${item.description || i18n.noDescription}`));
-                details = [
-                  p(`${i18n.agendaMembersCount}: ${item.members.length || 0}`),
-                  p(`${i18n.agendaLocationLabel}: ${item.location || i18n.noLocation}`),
-                  p(`${i18n.agendaLARPLabel}: ${item.isLARP ? i18n.agendaYes : i18n.agendaNo}`),
-                  p(`${i18n.agendaAnonymousLabel}: ${item.isAnonymous ? i18n.agendaYes : i18n.agendaNo}`),
-                  p(`${i18n.agendaInviteModeLabel}: ${item.inviteMode || i18n.noInviteMode}`)
-                ];
-                const membersList = item.members.map(member =>
-                  p(a({ href: `/author/${encodeURIComponent(member)}` }, member))
-                );
-                details.push(
-                  div({ class: 'members-list' }, `${i18n.agendaMembersLabel}:`, membersList)
-                );
-              }
-              if (item.type === 'report') {
-                details = [
-                  p(`${i18n.agendareportCategory}: ${item.category || i18n.noCategory}`),
-                  p(`${i18n.agendareportSeverity}: ${item.severity || i18n.noSeverity}`),
-                  p(`${i18n.agendareportStatus}: ${item.status || i18n.noStatus}`),
-                  p(`${i18n.agendareportDescription}: ${item.description || i18n.noDescription}`)
-                ];
-              }
-              if (item.type === 'event') {
-                details = [
-                  p(`${i18n.eventDescriptionLabel}: ${item.description}`),
-                  p(`${i18n.eventLocationLabel}: ${item.location}`),
-                  p(`${i18n.eventDateLabel}: ${fmt(item.date)}`),
-                  p(`${i18n.eventPriceLabel}: ${item.price} ECO`),
-                  p(`${i18n.eventUrlLabel}: ${item.url || i18n.noUrl}`)
-                ];
-                actionButton = actionButton || form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` },
-                  button({ type: 'submit', class: 'assign-btn' }, `${i18n.eventAttendButton}`));
-              }
-              if (item.type === 'task') {
-                details = [
-                  p(`${i18n.taskDescriptionLabel}: ${item.description}`),
-                  p(`${i18n.taskPriorityLabel}: ${item.priority}`),
-                  p(`${i18n.taskLocationLabel}: ${item.location}`)
-                ];
-                const assigned = Array.isArray(item.assignees) && item.assignees.includes(userId);
-                actionButton = actionButton || form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` },
-                  button({ type: 'submit', class: 'assign-btn' },
-                    assigned ? i18n.taskUnassignButton : i18n.taskAssignButton));
-              }
-              if (item.type === 'transfer') {
-                details = [
-                  p(`${i18n.agendaTransferConcept}: ${item.concept}`),
-                  p(`${i18n.agendaTransferAmount}: ${item.amount}`),
-                  p(`${i18n.agendaTransferDeadline}: ${fmt(item.deadline)}`)
-                ];
-              }
-
-              return div({ class: 'agenda-item' },
-                h2(`[${item.type.toUpperCase()}] ${item.title || item.name || item.concept}`),
-                ...commonFields,
-                ...details,
-                actionButton, br()
-              );
-            })
+          ? items.map(item => renderAgendaItem(item, userId, filter))
           : p(i18n.agendaNoItems)
       )
     )

+ 50 - 33
src/views/audio_view.js

@@ -1,4 +1,4 @@
-const { form, button, div, h2, p, section, input, label, br, a, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe");
+const { form, button, div, h2, p, section, input, label, br, a, audio: audioHyperaxe, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const moment = require("../server/node_modules/moment");
 const { config } = require('../server/SSB_server.js');
@@ -20,6 +20,12 @@ const getFilteredAudios = (filter, audios, userId) => {
   return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
 };
 
+const renderCardField = (label, value) =>
+  div({ class: "card-field" }, 
+    span({ class: "card-label" }, label), 
+    span({ class: "card-value" }, value)
+  );
+
 const renderAudioActions = (filter, audio) => {
   return filter === 'mine' ? div({ class: "audio-actions" },
     form({ method: "GET", action: `/audios/edit/${encodeURIComponent(audio.key)}` },
@@ -34,32 +40,36 @@ const renderAudioActions = (filter, audio) => {
 const renderAudioList = (filteredAudios, filter) => {
   return filteredAudios.length > 0
     ? filteredAudios.map(audio =>
-        div({ class: "audio-item" },
+        div({ class: "audio-item card" },
+         br,
           renderAudioActions(filter, audio),
           form({ method: "GET", action: `/audios/${encodeURIComponent(audio.key)}` },
             button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)),
-          br,
+          audio.title?.trim() ? h2(audio.title) : null,
           audio.url
             ? div({ class: "audio-container" },
                 audioHyperaxe({
                   controls: true,
                   src: `/blob/${encodeURIComponent(audio.url)}`,
                   type: audio.mimeType,
-                  preload: 'metadata',
-                  width: '640',
-                  height: '360'
+                  preload: 'metadata'
                 })
               )
             : p(i18n.audioNoFile),
-          p(`${i18n.audioCreatedAt}: ${moment(audio.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-          p(`${i18n.audioAuthor}: `, a({ href: `/author/${encodeURIComponent(audio.author)}` }, audio.author)),
-          audio.title?.trim() ? h2(audio.title) : null,
-          audio.description?.trim() ? p(audio.description) : null,
+          audio.description?.trim() ? renderCardField(`${i18n.audioDescriptionLabel}: `, audio.description) : null,
+          br,
           audio.tags?.length
-            ? div(audio.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-              ))
+            ? div({ class: "card-tags" }, 
+                audio.tags.map(tag =>
+                  a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
+                )
+              )
             : null,
+         br,
+         p({ class: 'card-footer' },
+           span({ class: 'date-link' }, `${moment(audio.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+           a({ href: `/author/${encodeURIComponent(audio.author)}`, class: 'user-link' }, `${audio.author}`)
+         ),
           div({ class: "voting-buttons" },
             ['interesting','necessary','funny','disgusting','sensible',
              'propaganda','adultOnly','boring','confusing','inspiring','spam']
@@ -70,7 +80,7 @@ const renderAudioList = (filteredAudios, filter) => {
                   )
                 )
               )
-          )
+          ),
         )
       )
     : div(i18n.noAudios);
@@ -154,38 +164,44 @@ exports.singleAudioView = async (audio, filter) => {
         )
       ),
       div({ class: "tags-header" },
+        isAuthor ? div({ class: "audio-actions" },
+        !hasOpinions
+          ? form({ method: "GET", action: `/audios/edit/${encodeURIComponent(audio.key)}` },
+              button({ class: "update-btn", type: "submit" }, i18n.audioUpdateButton)
+            )
+          : null,
+        form({ method: "POST", action: `/audios/delete/${encodeURIComponent(audio.key)}` },
+          button({ class: "delete-btn", type: "submit" }, i18n.audioDeleteButton)
+        )
+      ) : null,
+        form({ method: "GET", action: `/audios/${encodeURIComponent(audio.key)}` },
+        button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)),
         h2(audio.title),
-        p(audio.description),
         audio.url
           ? div({ class: "audio-container" },
               audioHyperaxe({
                 controls: true,
                 src: `/blob/${encodeURIComponent(audio.url)}`,
                 type: audio.mimeType,
-                preload: 'metadata',
-                width: '640',
-                height: '360'
+                preload: 'metadata'
               })
             )
           : p(i18n.audioNoFile),
-        p(`${i18n.audioCreatedAt}: ${moment(audio.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.audioAuthor}: `, a({ href: `/author/${encodeURIComponent(audio.author)}` }, audio.author)),
+        audio.description?.trim() ? renderCardField(`${i18n.audioDescriptionLabel}: `, audio.description) : null,
+        br,
         audio.tags?.length
-          ? div(audio.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      ),
-      isAuthor ? div({ class: "audio-actions" },
-        !hasOpinions
-          ? form({ method: "GET", action: `/audios/edit/${encodeURIComponent(audio.key)}` },
-              button({ class: "update-btn", type: "submit" }, i18n.audioUpdateButton)
+          ? div({ class: "card-tags" },
+              audio.tags.map(tag =>
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+              )
             )
           : null,
-        form({ method: "POST", action: `/audios/delete/${encodeURIComponent(audio.key)}` },
-          button({ class: "delete-btn", type: "submit" }, i18n.audioDeleteButton)
-        )
-      ) : null,
+          br,
+          p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(audio.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(audio.author)}`, class: 'user-link' }, `${audio.author}`)
+          ),
+      ),
       div({ class: "voting-buttons" },
         ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
           form({ method: "POST", action: `/audios/opinions/${encodeURIComponent(audio.key)}/${category}` },
@@ -196,3 +212,4 @@ exports.singleAudioView = async (audio, filter) => {
     )
   );
 };
+

+ 83 - 50
src/views/bookmark_view.js

@@ -1,4 +1,4 @@
-const { form, button, div, h2, p, section, input, label, textarea, br, a, ul, li } = require("../server/node_modules/hyperaxe");
+const { form, button, div, h2, p, section, input, label, textarea, br, a, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const moment = require("../server/node_modules/moment");
 const { config } = require('../server/SSB_server.js');
@@ -6,36 +6,56 @@ const { config } = require('../server/SSB_server.js');
 const userId = config.keys.id
 
 const renderBookmarkActions = (filter, bookmark) => {
-  return filter === 'mine' ? div({ class: "bookmark-actions" },
-    form({ method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` },
-      button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton)
-    ),
-    form({ method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` },
-      button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton)
-    )
-  ) : null;
+  return filter === 'mine'
+    ? div({ class: "bookmark-actions" },
+        form({ method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` },
+          button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton)
+        ),
+        form({ method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` },
+          button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton)
+        )
+      )
+    : null;
 };
 
+const renderCardField = (labelText, value) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, value)
+  );
+
 const renderBookmarkList = (filteredBookmarks, filter) => {
   return filteredBookmarks.length > 0
     ? filteredBookmarks.map(bookmark =>
-        div({ class: "bookmark-item" },
+        div({ class: "tags-header" },
           renderBookmarkActions(filter, bookmark),
           form({ method: "GET", action: `/bookmarks/${encodeURIComponent(bookmark.id)}` },
             button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-          ),br,
+          ),
           h2(bookmark.title),
-          p(bookmark.description),
-          label(bookmark.url ? a({ href: bookmark.url, target: "_blank", class: "bookmark-url" }, bookmark.url) : null), br,
-          p(`${i18n.bookmarkCreatedAt}: ${moment(bookmark.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-          p(`${i18n.bookmarkAuthor}: `, a({ href: `/author/${encodeURIComponent(bookmark.author)}` }, bookmark.author)),
-          bookmark.category?.trim() ? p(`${i18n.bookmarkCategory}: ${bookmark.category}`) : null,
-          p(`${i18n.bookmarkLastVisit}: ${moment(bookmark.lastVisit).format('YYYY/MM/DD HH:mm:ss') || i18n.noLastVisit}`),
+          renderCardField(i18n.bookmarkDescriptionLabel + ":", bookmark.description),
+          renderCardField(i18n.bookmarkUrlLabel + ":", bookmark.url
+            ? a({ href: bookmark.url, target: "_blank", class: "bookmark-url" }, bookmark.url)
+            : i18n.noUrl
+          ),
+          renderCardField(i18n.bookmarkLastVisit + ":", bookmark.lastVisit
+            ? moment(bookmark.lastVisit).format('YYYY/MM/DD HH:mm:ss')
+            : i18n.noLastVisit
+          ),
+          bookmark.category?.trim()
+            ? renderCardField(i18n.bookmarkCategory + ":", bookmark.category)
+            : null,
+          br,
           bookmark.tags?.length
-            ? div(bookmark.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+            ? div({ class: "card-tags" }, bookmark.tags.map(tag =>
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
               ))
             : null,
+          br,
+          div({ class: 'card-footer' },
+            span({ class: 'date-link' }, `${moment(bookmark.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+            a({ href: `/author/${encodeURIComponent(bookmark.author)}`, class: 'user-link' }, `${bookmark.author}`)
+          ),
           div({ class: "voting-buttons" },
             ['interesting','necessary','funny','disgusting','sensible','propaganda','adultOnly','boring','confusing','inspiring','spam'].map(category =>
               form({ method: "POST", action: `/bookmarks/opinions/${encodeURIComponent(bookmark.id)}/${category}` },
@@ -45,11 +65,11 @@ const renderBookmarkList = (filteredBookmarks, filter) => {
           )
         )
       )
-    : i18n.nobookmarks;
+    : p(i18n.nobookmarks);
 };
 
 const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags) => {
-  return div({ class: "div-center bookmark-form" },
+  return div({ class: "div-center bookmark-form" },   // <-- No "card" here
     form(
       {
         action: filter === 'edit'
@@ -59,13 +79,13 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags) => {
       },
       label(i18n.bookmarkUrlLabel), br,
       input({ type: "url", name: "url", id: "url", required: true, placeholder: i18n.bookmarkUrlPlaceholder, value: filter === 'edit' ? bookmarkToEdit.url : '' }), br, br,
-      label(i18n.bookmarkDescriptionLabel),
-      textarea({ name: "description", id: "description", placeholder: i18n.bookmarkDescriptionPlaceholder, value: filter === 'edit' ? bookmarkToEdit.description : '' }), br, br,
-      label(i18n.bookmarkTagsLabel),
+      label(i18n.bookmarkDescriptionLabel), br,
+      textarea({ name: "description", id: "description", placeholder: i18n.bookmarkDescriptionPlaceholder }, filter === 'edit' ? bookmarkToEdit.description : ''), br, br,
+      label(i18n.bookmarkTagsLabel), br,
       input({ type: "text", name: "tags", id: "tags", placeholder: i18n.bookmarkTagsPlaceholder, value: filter === 'edit' ? tags.join(', ') : '' }), br, br,
-      label(i18n.bookmarkCategoryLabel),
+      label(i18n.bookmarkCategoryLabel), br,
       input({ type: "text", name: "category", id: "category", placeholder: i18n.bookmarkCategoryPlaceholder, value: filter === 'edit' ? bookmarkToEdit.category : '' }), br, br,
-      label(i18n.bookmarkLastVisitLabel),
+      label(i18n.bookmarkLastVisitLabel), br,
       input({ type: "datetime-local", name: "lastVisit", value: filter === 'edit' ? moment(bookmarkToEdit.lastVisit).format('YYYY-MM-DDTHH:mm:ss') : '' }), br, br,
       button({ type: "submit" }, filter === 'edit' ? i18n.bookmarkUpdateButton : i18n.bookmarkCreateButton)
     )
@@ -153,38 +173,51 @@ exports.singleBookmarkView = async (bookmark, filter) => {
           button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton)
         )
       ),
-      div({ class: "tags-header" },
-        p(bookmark.description),
-        label(bookmark.url ? a({ href: bookmark.url, target: "_blank", class: "bookmark-url" }, bookmark.url) : null), br,
-        p(`${i18n.bookmarkCreatedAt}: ${moment(bookmark.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.bookmarkAuthor}: `, a({ href: `/author/${encodeURIComponent(bookmark.author)}` }, bookmark.author)),
-        p(`${i18n.bookmarkCategory}: ${bookmark.category || i18n.noCategory}`),
-        p(`${i18n.bookmarkLastVisitLabel}: ${moment(bookmark.lastVisit).format('YYYY/MM/DD HH:mm:ss') || i18n.noLastVisit}`),
+      div({ class: "bookmark-item card" },
+      br,
+          isAuthor ? div({ class: "bookmark-actions" },
+          !hasOpinions
+            ? form({ method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` },
+                button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton)
+              )
+            : null,
+          form({ method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` },
+            button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton)
+          )
+        ) : null,
+        h2(bookmark.title),
+        renderCardField(i18n.bookmarkDescriptionLabel + ":", bookmark.description),
+        renderCardField(i18n.bookmarkUrlLabel + ":", bookmark.url
+          ? a({ href: bookmark.url, target: "_blank", class: "bookmark-url" }, bookmark.url)
+          : i18n.noUrl
+        ),
+        renderCardField(i18n.bookmarkLastVisit + ":", bookmark.lastVisit
+          ? moment(bookmark.lastVisit).format('YYYY/MM/DD HH:mm:ss')
+          : i18n.noLastVisit
+        ),
+        renderCardField(i18n.bookmarkCategory + ":", bookmark.category || i18n.noCategory),
+        br,
         bookmark.tags && bookmark.tags.length
-          ? div(
+          ? div({ class: "card-tags" },
               bookmark.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
               )
             )
-          : null
-      ),
-      isAuthor ? div({ class: "bookmark-actions" },
-        !hasOpinions
-          ? form({ method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` },
-              button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton)
-            )
           : null,
-        form({ method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` },
-          button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton)
-        )
-      ) : null,
-      div({ class: "voting-buttons" },
-        ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
-          form({ method: "POST", action: `/bookmarks/opinions/${encodeURIComponent(bookmark.id)}/${category}` },
-            button({ class: "vote-btn" }, `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${bookmark.opinions?.[category] || 0}]`)
+        br,
+        div({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(bookmark.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(bookmark.author)}`, class: 'user-link' }, `${bookmark.author}`)
+        ),
+        div({ class: "voting-buttons" },
+          ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
+            form({ method: "POST", action: `/bookmarks/opinions/${encodeURIComponent(bookmark.id)}/${category}` },
+              button({ class: "vote-btn" }, `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${bookmark.opinions?.[category] || 0}]`)
+            )
           )
         )
       )
     )
   );
 };
+

+ 3 - 3
src/views/cv_view.js

@@ -154,9 +154,9 @@ exports.cvView = async (cv) => {
                   class: "cv-photo"
                 })
               : null,
-            cv.contact ? p("User ID: ", a({ href: `/author/${encodeURIComponent(cv.contact)}` }, cv.contact)) : null,
-            cv.name ? p(`${i18n.cvNameLabel}: ${cv.name}`) : null,
-            cv.description ? p(`${i18n.cvDescriptionLabel}: ${cv.description}`) : null,
+            cv.name ? h2(`${cv.name}`) : null,
+            cv.contact ? p(a({ class: "user-link", href: `/author/${encodeURIComponent(cv.contact)}` }, cv.contact)) : null,
+            cv.description ? p(`${cv.description}`) : null,
             cv.languages ? p(`${i18n.cvLanguagesLabel}: ${cv.languages}`) : null,
             (cv.personalSkills && cv.personalSkills.length)
               ? div(

+ 29 - 24
src/views/document_view.js

@@ -1,9 +1,9 @@
-const { form, button, div, h2, p, section, input, label, br, a } = require("../server/node_modules/hyperaxe");
+const { form, button, div, h2, p, section, input, label, br, a, span } = require("../server/node_modules/hyperaxe");
 const moment = require("../server/node_modules/moment");
 const { template, i18n } = require('./main_views');
 const { config } = require('../server/SSB_server.js');
 
-const userId = config.keys.id
+const userId = config.keys.id;
 
 const getFilteredDocuments = (filter, documents, userId) => {
   const now = Date.now();
@@ -34,25 +34,27 @@ const renderDocumentActions = (filter, doc) => {
 const renderDocumentList = (filteredDocs, filter) => {
   return filteredDocs.length > 0
     ? filteredDocs.map(doc =>
-        div({ class: "document-item" },
+        div({ class: "tags-header" },
           renderDocumentActions(filter, doc),
           form({ method: "GET", action: `/documents/${encodeURIComponent(doc.key)}` },
             button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)),
-          br,
+          doc.title?.trim() ? h2(doc.title) : null,
           div({
             id: `pdf-container-${doc.key}`,
             class: 'pdf-viewer-container',
             'data-pdf-url': `/blob/${encodeURIComponent(doc.url)}`
           }),
-          p(`${i18n.documentCreatedAt}: ${moment(doc.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-          p(`${i18n.documentAuthor}: `, a({ href: `/author/${encodeURIComponent(doc.author)}` }, doc.author)),
-          doc.title?.trim() ? h2(doc.title) : null,
           doc.description?.trim() ? p(doc.description) : null,
           doc.tags.length
-            ? div(doc.tags.map(tag =>
+            ? div({ class: "card-tags" }, doc.tags.map(tag =>
                 a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
               ))
             : null,
+          br,
+          p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(doc.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(doc.author)}`, class: 'user-link' }, `${doc.author}`)
+          ),
           div({ class: "voting-buttons" },
             ['interesting','necessary','funny','disgusting','sensible',
              'propaganda','adultOnly','boring','confusing','inspiring','spam']
@@ -149,22 +151,7 @@ exports.singleDocumentView = async (doc, filter) => {
         )
       ),
       div({ class: "tags-header" },
-        h2(doc.title),
-        p(doc.description),
-        div({
-          id: `pdf-container-${doc.key}`,
-          class: 'pdf-viewer-container',
-          'data-pdf-url': `/blob/${encodeURIComponent(doc.url)}`
-        }),
-        p(`${i18n.documentCreatedAt}: ${moment(doc.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.documentAuthor}: `, a({ href: `/author/${encodeURIComponent(doc.author)}` }, doc.author)),
-        doc.tags?.length
-          ? div(doc.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-            ))
-          : null
-      ),
-      isAuthor ? div({ class: "document-actions" },
+       isAuthor ? div({ class: "document-actions" },
         !hasOpinions
           ? form({ method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` },
               button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton)
@@ -174,6 +161,24 @@ exports.singleDocumentView = async (doc, filter) => {
           button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton)
         )
       ) : null,
+        h2(doc.title),
+        div({
+          id: `pdf-container-${doc.key}`,
+          class: 'pdf-viewer-container',
+          'data-pdf-url': `/blob/${encodeURIComponent(doc.url)}`
+        }),
+        p(doc.description),
+          doc.tags.length
+            ? div({ class: "card-tags" }, doc.tags.map(tag =>
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
+              ))
+            : null,
+            br,
+          p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(doc.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(doc.author)}`, class: 'user-link' }, `${doc.author}`)
+          ),
+      ),
       div({ class: "voting-buttons" },
         ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
           form({ method: "POST", action: `/documents/opinions/${encodeURIComponent(doc.key)}/${category}` },

+ 110 - 88
src/views/event_view.js

@@ -5,6 +5,78 @@ const { config } = require('../server/SSB_server.js');
 
 const userId = config.keys.id
 
+const renderStyledField = (labelText, valueElement) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, valueElement)
+  );
+
+const renderEventItem = (e, filter, userId) => {
+  const actions = [];
+  if (filter === 'mine' && e.status === 'OPEN') {
+    actions.push(
+      form({ method:"GET", action:`/events/edit/${encodeURIComponent(e.id)}` },
+        button({ type:"submit", class:"update-btn" }, i18n.eventUpdateButton)
+      ),
+      form({ method:"POST", action:`/events/delete/${encodeURIComponent(e.id)}` },
+        button({ type:"submit", class:"delete-btn" }, i18n.eventDeleteButton)
+      )
+    );
+  }
+  if (e.status === 'OPEN') {
+    actions.push(
+      form({ method:"POST", action:`/events/attend/${encodeURIComponent(e.id)}` },
+        button({ type:"submit" },
+          e.attendees.includes(userId)
+            ? i18n.eventUnattendButton
+            : i18n.eventAttendButton
+        )
+      )
+    );
+  }
+  return div({ class:"card card-section event" },
+    actions.length ? div({ class:"event-actions" }, ...actions) : null,
+    form({ method:"GET", action:`/events/${encodeURIComponent(e.id)}` },
+      button({ type:"submit", class:"filter-btn" }, i18n.viewDetails)
+    ),
+    br,
+    renderStyledField(i18n.eventTitleLabel + ':', e.title),
+    renderStyledField(i18n.eventDescriptionLabel + ':', e.description),
+    renderStyledField(i18n.eventDateLabel + ':', moment(e.date).format('YYYY/MM/DD HH:mm:ss')),
+    e.location?.trim() ? renderStyledField(i18n.eventLocationLabel + ':', e.location) : null,
+    renderStyledField(i18n.eventPrivacyLabel + ':', e.isPublic.toUpperCase()),
+    renderStyledField(i18n.eventStatus + ':', e.status),
+    e.url?.trim() ? renderStyledField(i18n.eventUrlLabel + ':', a({ href: e.url }, e.url)) : null,
+    renderStyledField(i18n.eventPriceLabel + ':', parseFloat(e.price || 0).toFixed(6) + ' ECO'),
+    br,
+    div({ class: 'card-field' },
+      span({ class: 'card-label' }, i18n.eventAttendees + ':'),
+      span({ class: 'card-value' },
+        Array.isArray(e.attendees) && e.attendees.length
+          ? e.attendees.filter(Boolean).map((id, i) => [i > 0 ? ', ' : '', a({ class: "user-link", href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
+          : i18n.noAttendees
+      )
+    ),
+    br,
+    e.tags && e.tags.filter(Boolean).length
+      ? div({ class: 'card-tags' },
+          e.tags.filter(Boolean).map(tag =>
+            a({
+              href:`/search?query=%23${encodeURIComponent(tag)}`,
+              class:"tag-link"
+            }, `#${tag}`)
+          )
+        )
+      : null,
+    br,
+    p({ class: 'card-footer' },
+      span({ class: 'date-link' }, `${e.createdAt} ${i18n.performed} `),
+      a({ href: `/author/${encodeURIComponent(e.organizer)}`, class: 'user-link' }, `${e.organizer}`)
+    )
+    
+  )
+};
+
 exports.eventView = async (events, filter, eventId) => {
   const list = Array.isArray(events) ? events : [events]
   const title =
@@ -116,73 +188,7 @@ exports.eventView = async (events, filter, eventId) => {
       ) : (
         div({ class:"event-list" },
           filtered.length > 0
-            ? filtered.map(e => {
-                const actions = []
-                if (filter==='mine' && e.status==='OPEN') {
-                  actions.push(
-                    form({ method:"GET", action:`/events/edit/${encodeURIComponent(e.id)}` },
-                      button({ type:"submit", class:"update-btn" }, i18n.eventUpdateButton)
-                    )
-                  )
-                  actions.push(
-                    form({ method:"POST", action:`/events/delete/${encodeURIComponent(e.id)}` },
-                      button({ type:"submit", class:"delete-btn" }, i18n.eventDeleteButton)
-                    )
-                  )
-                }
-                if (e.status === 'OPEN') {
-                  actions.push(
-                    form({ method:"POST", action:`/events/attend/${encodeURIComponent(e.id)}` },
-                      button({ type:"submit" },
-                        e.attendees.includes(userId)
-                          ? i18n.eventUnattendButton
-                          : i18n.eventAttendButton
-                      )
-                    )
-                  )
-                }
-                return div({ class:"event-item" },
-                  actions.length ? div({ class:"event-actions" }, ...actions) : null,
-                  form({ method:"GET", action:`/events/${encodeURIComponent(e.id)}` },
-                    button({ type:"submit", class:"filter-btn" }, i18n.viewDetails)
-                  ),
-                  h2(e.title),
-                  p(`${i18n.eventDescription}: ${e.description}`),
-                  p(`${i18n.eventDate}: ${moment(e.date).format('YYYY/MM/DD HH:mm:ss')}`),
-                  p(`${i18n.eventPrivacyLabel}: ${e.isPublic}`),
-                  e.location?.trim() ? p(`${i18n.eventLocation}: ${e.location}`) : null,
-                  e.url?.trim() ? p(`${i18n.eventUrlLabel}: `, a({ href: e.url }, e.url)) : null,
-                  p(`${i18n.eventPriceLabel}: ${parseFloat(e.price || 0).toFixed(6)} ECO`),
-                  p(`${i18n.eventAttendees}: `,
-                    Array.isArray(e.attendees) && e.attendees.length
-                      ? e.attendees.filter(Boolean).map((id, i) => [i > 0 ? ', ' : '', a({ href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
-                      : i18n.noAttendees
-                  ),
-                  p(`${i18n.eventCreatedAt}: ${moment(e.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-                  p(`${i18n.eventBy}: `, a({ href: `/author/${encodeURIComponent(e.organizer)}` }, e.organizer)),
-                  p(`${i18n.eventStatus}: ${e.status}`),
-                  e.tags && e.tags.filter(Boolean).length
-                    ? div(
-                        e.tags.filter(Boolean).map(tag =>
-                          a({
-                            href:`/search?query=%23${encodeURIComponent(tag)}`,
-                            class:"tag-link",
-                            style:"margin-right:0.8em;margin-bottom:0.5em;"
-                          }, `#${tag}`)
-                        )
-                      )
-                    : null,
-                  div({ class: "voting-buttons" },
-                    ['interesting','necessary','funny','disgusting','sensible','propaganda','adultOnly','boring','confusing','inspiring','spam'].map(category =>
-                      form({ method:"POST", action:`/events/opinions/${encodeURIComponent(e.id)}/${category}` },
-                        button({ class:"vote-btn" },
-                          `${i18n[`vote${category.charAt(0).toUpperCase()+category.slice(1)}`]} [${e.opinions?.[category]||0}]`
-                        )
-                      )
-                    )
-                  )
-                )
-              })
+            ? filtered.map(e => renderEventItem(e, filter, userId))
             : p(i18n.noevents)
         )
       )
@@ -206,27 +212,6 @@ exports.singleEventView = async (event, filter) => {
           button({ type: 'submit', name: 'filter', value: 'create', class: "create-button" }, i18n.eventCreateButton)
         )
       ),
-      div({ class: "tags-header" },
-        h2(event.title),
-        p(event.description),
-        p(`${i18n.eventDate}: ${moment(event.date).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.eventLocation}: ${event.location}`),
-        p(`${i18n.eventUrlLabel}: `, a({ href: event.url }, event.url)),
-        p(`${i18n.eventPriceLabel}: ${parseFloat(event.price || 0).toFixed(6)} ECO`),
-        p(`${i18n.eventAttendees}: `,
-          Array.isArray(event.attendees) && event.attendees.length
-            ? event.attendees.map((id, i) => [i > 0 ? ', ' : '', a({ href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
-            : i18n.noAttendees
-        ),
-        p(`${i18n.eventCreatedAt}: ${moment(event.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.eventBy}: `, a({ href: `/author/${encodeURIComponent(event.organizer)}` }, event.organizer)),
-        p(`${i18n.eventStatus}: ${event.status}`),
-        event.tags && event.tags.length
-          ? div(
-              event.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))
-            )
-          : null
-      ),
       div({ class: "event-actions" },
         form({ method: "POST", action: `/events/attend/${encodeURIComponent(event.id)}` },
           button({ type: "submit" },
@@ -235,7 +220,44 @@ exports.singleEventView = async (event, filter) => {
               : i18n.eventAttendButton
           )
         )
+      ),
+      div({ class: "card card-section event" },
+        form({ method:"GET", action:`/events/${encodeURIComponent(event.id)}` },
+         button({ type:"submit", class:"filter-btn" }, i18n.viewDetails)
+        ),
+        br,
+        renderStyledField(i18n.eventTitleLabel + ':', event.title),
+        renderStyledField(i18n.eventDescriptionLabel + ':', event.description),
+        renderStyledField(i18n.eventDateLabel + ':', moment(event.date).format('YYYY/MM/DD HH:mm:ss')),
+        event.location?.trim() ? renderStyledField(i18n.eventLocationLabel + ':', event.location) : null,
+        renderStyledField(i18n.eventPrivacyLabel + ':', event.isPublic.toUpperCase()),
+        renderStyledField(i18n.eventStatus + ':', event.status),
+        event.url?.trim() ? renderStyledField(i18n.eventUrlLabel + ':', a({ href: event.url }, event.url)) : null,
+        renderStyledField(i18n.eventPriceLabel + ':', parseFloat(event.price || 0).toFixed(6) + ' ECO'),
+        br,
+        div({ class: 'card-field' },
+          span({ class: 'card-label' }, i18n.eventAttendees + ':'),
+          span({ class: 'card-value' },
+            Array.isArray(event.attendees) && event.attendees.length
+              ? event.attendees.filter(Boolean).map((id, i) => [i > 0 ? ', ' : '', a({ class: "user-link", href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
+              : i18n.noAttendees
+          )
+        ),
+        br,
+        event.tags && event.tags.length
+          ? div({ class: 'card-tags' },
+              event.tags.filter(Boolean).map(tag =>
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+              )
+            )
+          : null,
+              br,
+        p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(event.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(event.organizer)}`, class: 'user-link' }, `${event.organizer}`)
+        )
       )
     )
   );
 };
+

+ 5 - 3
src/views/feed_view.js

@@ -31,9 +31,11 @@ const renderFeedCard = (feed, alreadyRefeeded, alreadyVoted) => {
       ),
       div({ class: 'feed-main' },
         div({ class: 'feed-text', innerHTML: renderTextWithStyles(content.text) }),
-        p(`${i18n.author}: `, a({ href: `/author/${encodeURIComponent(feed.value.author)}` }, feed.value.author)),
-        p(`${i18n.createdAtLabel}: ${createdAt}`),
-        h2(`${i18n.totalOpinions}: ${totalCount}`)
+        h2(`${i18n.totalOpinions}: ${totalCount}`),
+        p({ class: 'card-footer' },
+        span({ class: 'date-link' }, `${createdAt} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(feed.value.author)}`, class: 'user-link' }, `${feed.value.author}`)
+        )
       )
     ),
     div({ class: 'votes-wrapper' },

+ 38 - 29
src/views/image_view.js

@@ -1,9 +1,9 @@
-const { form, button, div, h2, p, section, input, label, br, a, img } = require("../server/node_modules/hyperaxe");
+const { form, button, div, h2, p, section, input, label, br, a, img, span } = require("../server/node_modules/hyperaxe");
 const moment = require("../server/node_modules/moment");
 const { template, i18n } = require('./main_views');
 const { config } = require('../server/SSB_server.js');
 
-const userId = config.keys.id
+const userId = config.keys.id;
 
 const getFilteredImages = (filter, images, userId) => {
   const now = Date.now();
@@ -11,7 +11,7 @@ const getFilteredImages = (filter, images, userId) => {
     filter === 'mine' ? images.filter(img => img.author === userId) :
     filter === 'recent' ? images.filter(img => new Date(img.createdAt).getTime() >= now - 86400000) :
     filter === 'meme' ? images.filter(img => img.meme) :
-    filter === 'top' ? [...images].sort((a,b) => {
+    filter === 'top' ? [...images].sort((a, b) => {
       const sum = o => Object.values(o || {}).reduce((s, n) => s + n, 0);
       return sum(b.opinions) - sum(a.opinions);
     }) :
@@ -34,22 +34,27 @@ const renderImageActions = (filter, imgObj) => {
 const renderImageList = (filteredImages, filter) => {
   return filteredImages.length > 0
     ? filteredImages.map(imgObj =>
-        div({ class: "image-item" },
-          renderImageActions(filter, imgObj),
+        div({ class: "tags-header" },
+         renderImageActions(filter, imgObj),
           form({ method: "GET", action: `/images/${encodeURIComponent(imgObj.key)}` },
-	    button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-	  ),  br,
-          a({ href: `#img-${encodeURIComponent(imgObj.key)}` }, img({ src: `/blob/${encodeURIComponent(imgObj.url)}` })),
-          br(),
-          p(`${i18n.imageCreatedAt}: ${moment(imgObj.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-          p(`${i18n.imageAuthor}: `, a({ href: `/author/${encodeURIComponent(imgObj.author)}` }, imgObj.author)),
+            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+          ),
+          
           imgObj.title ? h2(imgObj.title) : null,
+          a({ href: `#img-${encodeURIComponent(imgObj.key)}` }, img({ src: `/blob/${encodeURIComponent(imgObj.url)}` })),
           imgObj.description ? p(imgObj.description) : null,
           imgObj.tags?.length
-            ? div(imgObj.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-              ))
+            ? div({ class: "card-tags" }, 
+                imgObj.tags.map(tag =>
+                  a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+                )
+              )
             : null,
+          br,
+          p({ class: 'card-footer' },
+            span({ class: 'date-link' }, `${moment(imgObj.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+            a({ href: `/author/${encodeURIComponent(imgObj.author)}`, class: 'user-link' }, `${imgObj.author}`)
+          ),
           div({ class: "voting-buttons" },
             ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam']
               .map(category =>
@@ -161,7 +166,7 @@ exports.imageView = async (images, filter, imageId) => {
 
 exports.singleImageView = async (image, filter) => {
   const isAuthor = image.author === userId;
-  const hasOpinions = Object.keys(image.opinions || {}).length > 0; 
+  const hasOpinions = Object.keys(image.opinions || {}).length > 0;
 
   return template(
     i18n.imageTitle,
@@ -177,20 +182,7 @@ exports.singleImageView = async (image, filter) => {
         )
       ),
       div({ class: "tags-header" },
-        h2(image.title),
-        p(image.description),
-        p(`${i18n.imageCreatedAt}: ${moment(image.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.imageAuthor}: `, a({ href: `/author/${encodeURIComponent(image.author)}` }, image.author)),
-        image.url ? img({ src: `/blob/${encodeURIComponent(image.url)}` }) : null,br,
-        image.tags?.length
-          ? div(
-              image.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-              )
-            )
-          : null
-      ),
-      isAuthor ? div({ class: "image-actions" },
+        isAuthor ? div({ class: "image-actions" },
         !hasOpinions
           ? form({ method: "GET", action: `/images/edit/${encodeURIComponent(image.key)}` },
               button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton)
@@ -200,6 +192,22 @@ exports.singleImageView = async (image, filter) => {
           button({ class: "delete-btn", type: "submit" }, i18n.imageDeleteButton)
         )
       ) : null,
+        h2(image.title),
+        image.url ? img({ src: `/blob/${encodeURIComponent(image.url)}` }) : null,
+        p(image.description),
+        image.tags?.length
+            ? div({ class: "card-tags" }, 
+              image.tags.map(tag =>
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+              )
+            )
+          : null,
+          br,
+          p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(image.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(image.author)}`, class: 'user-link' }, `${image.author}`)
+          ),
+      ),
       div({ class: "voting-buttons" },
         ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
           form({ method: "POST", action: `/images/opinions/${encodeURIComponent(image.key)}/${category}` },
@@ -210,3 +218,4 @@ exports.singleImageView = async (image, filter) => {
     )
   );
 };
+

+ 3 - 3
src/views/inhabitants_view.js

@@ -38,7 +38,7 @@ const renderInhabitantCard = (user, filter) => {
         ? p(`${i18n.mutualFollowers}: ${user.mutualCount}`) : null,
       filter === 'blocked' && user.isBlocked
         ? p(i18n.blockedLabel) : null,
-      p(`${i18n.oasisId}: `, a({ href: `/author/${encodeURIComponent(user.id)}` }, user.id)),
+      p(a({ class: 'user-link', href: `/author/${encodeURIComponent(user.id)}` }, user.id)),
       ['CVs', 'MATCHSKILLS', 'SUGGESTED'].includes(filter)
         ? a({ href: `/inhabitant/${encodeURIComponent(user.id)}`, class: 'view-profile-btn' }, i18n.inhabitantviewDetails)
         : null
@@ -185,7 +185,7 @@ exports.inhabitantsProfileView = ({ about = {}, cv = {}, feed = [] }) => {
         img({ class: 'inhabitant-photo', src: image, alt: name }),
         div({ class: 'inhabitant-details' },
           h2(name),
-          p(a({ href: `/author/${encodeURIComponent(id)}` }, id)),
+          p(a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id)),
           description ? p(description) : null,
           location ? p(`${i18n.locationLabel}: ${location}`) : null,
           languages.length ? p(`${i18n.languagesLabel}: ${languages.join(', ')}`) : null,
@@ -197,7 +197,7 @@ exports.inhabitantsProfileView = ({ about = {}, cv = {}, feed = [] }) => {
       ),
       feed && feed.length
         ? section({ class: 'profile-feed' },
-            h2(i18n.latestMessages || 'Latest Messages'),
+            h2(i18n.latestInteractions),
             feed.map(m => div({ class: 'post' }, p(m.value.content.text || '')))
           )
         : null

+ 14 - 19
src/views/main_views.js

@@ -236,14 +236,8 @@ const renderMarketLink = () => {
   const marketMod = getConfig().modules.marketMod === 'on';
   return marketMod 
     ? [
-        div(
-          { class: "top-bar-mid" },
-          nav(
-            ul(
-              navLink({ href: "/market", emoji: "ꕻ", text: i18n.marketTitle })
-            )
-          )
-        )
+      hr(),
+      navLink({ href: "/market", emoji: "ꕻ", text: i18n.marketTitle }),
       ]
     : '';
 };
@@ -299,7 +293,6 @@ const renderTransfersLink = () => {
   return transfersMod 
     ? [
       navLink({ href: "/transfers", emoji: "ꘉ", text: i18n.transfersTitle, class: "transfers-link enabled" }),
-      hr(),
       ]
     : '';
 };
@@ -308,6 +301,7 @@ const renderFeedLink = () => {
   const feedMod = getConfig().modules.feedMod === 'on';
   return feedMod 
     ? [
+      hr(),
       navLink({ href: "/feed", emoji: "ꕿ", text: i18n.feedTitle, class: "feed-link enabled" }),
       ]
     : '';
@@ -388,7 +382,6 @@ const template = (titlePrefix, ...elements) => {
             )
           )
         ),
-        renderMarketLink(),
         div(
           { class: "top-bar-right" },
           nav(
@@ -437,9 +430,10 @@ const template = (titlePrefix, ...elements) => {
               navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }),
               renderTrendingLink(),
               renderOpinionsLink(),
-              renderTransfersLink(),
               renderFeedLink(),
               renderPixeliaLink(),
+              renderMarketLink(),
+              renderTransfersLink(),
               renderBookmarksLink(),
               renderImagesLink(),
               renderVideosLink(),
@@ -1111,7 +1105,7 @@ const renderMessage = (msg) => {
 
   return div({ class: "mention-item" }, [
     div({ class: "mention-content", innerHTML: mentionsText || '[No content]' }),
-    p(`By: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)),
+    p(a({ class: 'user-link', href: `/author/${encodeURIComponent(author)}` }, author)),
     p(`${i18n.createdAtLabel || i18n.mentionsCreatedAt}: ${createdAt}`)
   ]);
 };
@@ -1229,15 +1223,16 @@ exports.privateView = async (input, filter) => {
               const sentAt = new Date(content.sentAt || msg.timestamp).toLocaleString();
               const from = content.from;
               const toLinks = (content.to || []).map(addr =>
-                a({ href: `/author/${encodeURIComponent(addr)}` }, addr)
+                a({ class: 'user-link', href: `/author/${encodeURIComponent(addr)}` }, addr)
               );
 
               return div({ class: 'message-item' },
-                h2(subject),
-                p(`${i18n.privateFrom}: `, a({ href: `/author/${encodeURIComponent(from)}` }, from)),
-                p(`${i18n.privateTo}: [ `, ...toLinks.flatMap((link, i, arr) => i < arr.length - 1 ? [link, ', '] : [link]), ' ]'),
-                p(`${i18n.privateDate}: ${sentAt}`),
+                p(subject),
                 div({ class: 'message-text' }, text),
+                p({ class: 'card-footer' },
+                span({ class: 'date-link' }, `${sentAt} ${i18n.performed} `),
+                 a({ href: `/author/${encodeURIComponent(from)}`, class: 'user-link' }, `${from}`)
+                ),
                 form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(msg.key)}`, class: 'delete-message-form' },
                   button({ type: 'submit', class: 'delete-btn' }, i18n.privateDelete)
                 )
@@ -1294,7 +1289,7 @@ exports.threadView = ({ messages }) => {
   const rootSnippet = postSnippet(
     lodash.get(rootMessage, "value.content.text", i18n.mysteryDescription)
   );
-  return template([`@${rootAuthorName}: `, rootSnippet], 
+  return template([`@${rootAuthorName}`], 
     div(
     thread(messages)
     )
@@ -1440,7 +1435,7 @@ const generatePreview = ({ previewData, contentWarning, action }) => {
             { class: "mention-relationship-details" },
             span({ class: "emoji" }, matches[0].rel.followsMe ? "☍" : "⚼"),
             span({ class: "mentions-listing" },
-              a({ href: `/author/@${encodeURIComponent(matches[0].feed)}` }, `@${matches[0].feed}`)
+              a({ class: 'user-link', href: `/author/@${encodeURIComponent(matches[0].feed)}` }, `@${matches[0].feed}`)
             )
           )
         );

+ 120 - 92
src/views/market_view.js

@@ -5,6 +5,12 @@ const { config } = require('../server/SSB_server.js');
 
 const userId = config.keys.id;
 
+const renderCardField = (labelText, value) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, value)
+  );
+
 exports.marketView = async (items, filter, itemToEdit = null) => {
   const list = Array.isArray(items) ? items : [];
   let title = i18n.marketAllSectionTitle;
@@ -118,6 +124,17 @@ exports.marketView = async (items, filter, itemToEdit = null) => {
               option({ value: "NEW", selected: itemToEdit?.item_status === 'NEW' ? true : false }, "NEW")
             ), br(), br(),
             
+            label(i18n.marketItemStock), br(),
+	    input({ 
+	      type: "number", 
+	      name: "stock", 
+	      id: "stock", 
+	      value: itemToEdit?.stock || 1, 
+	      required: true, 
+	      min: "1", 
+	      step: "1" 
+	    }), br(), br(),
+            
             label(i18n.marketItemPrice), br(),
             input({ type: "number", name: "price", id: "price", value: itemToEdit?.price || '', required: true, step: "0.000001", min: "0.000001" }), br(), br(),
             
@@ -141,95 +158,99 @@ exports.marketView = async (items, filter, itemToEdit = null) => {
           )
         )
       ) : (
-        div({ class: "market-grid" },
-          filtered.length > 0
-            ? filtered.map((item, index) =>     
-              div({ class: "market-item" }, 
-                div({ class: "market-card left-col" },
-                  form({ method: "GET", action: `/market/${encodeURIComponent(item.id)}` },
-                      button({ class: "filter-btn", type: "submit" }, i18n.viewDetails)
-                  ),
-                  h2({ class: "market-card type" }, `${i18n.marketItemType}: ${item.item_type}`),
-                  p(item.title),
-
-                  p(item.description),
-                  div({ class: "market-card image" },
-                    item.image
-                      ? img({ src: `/blob/${encodeURIComponent(item.image)}` })
-                      : img({ src: '/assets/images/default-market.png', alt: item.title })
-                  ),
-                  item.tags && item.tags.filter(Boolean).length
-                    ? item.tags.filter(Boolean).map(tag =>
-                        a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` },
-                          `#${tag}`)
-                      )
-                    : null
-                ),
-                div({ class: "market-card right-col" },
-                  div({ class: "market-card price" },
-                    h2(`${i18n.marketItemPrice}: ${item.price} ECO`)
-                  ),
-                  p(`${i18n.marketItemCondition}: ${item.item_status}`),
-                  p(`${i18n.marketItemIncludesShipping}: ${item.includesShipping ? i18n.YESLabel : i18n.NOLabel}`),
-                  p(`${i18n.marketItemStatus}: ${item.status}`),
-                  item.deadline ? p(`${i18n.marketItemAvailable}: ${moment(item.deadline).format('YYYY/MM/DD HH:mm:ss')}`) : null,
+	div({ class: "market-grid" },
+	  filtered.length > 0
+	    ? filtered.map((item, index) =>     
+	      div({ class: "market-item" }, 
+		div({ class: "market-card left-col" },
+		  form({ method: "GET", action: `/market/${encodeURIComponent(item.id)}` },
+		      button({ class: "filter-btn", type: "submit" }, i18n.viewDetails)
+		  ),
+		  h2({ class: "market-card type" }, `${i18n.marketItemType}: ${item.item_type.toUpperCase()}`),
+		  h2(item.title),
+		  renderCardField(`${i18n.marketItemStatus}:`, item.status),
+		  item.deadline ? renderCardField(`${i18n.marketItemAvailable}:`, moment(item.deadline).format('YYYY/MM/DD HH:mm:ss')) : null,
+		  br,br,
+		  div({ class: "market-card image" },
+		    item.image
+		      ? img({ src: `/blob/${encodeURIComponent(item.image)}` })
+		      : img({ src: '/assets/images/default-market.png', alt: item.title })
+		  ),
+		  p(item.description),
+		  item.tags && item.tags.filter(Boolean).length
+		    ? div({ class: 'card-tags' }, item.tags.filter(Boolean).map(tag =>
+		        a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` },
+		          `#${tag}`)
+		      ))
+		    : null,
+		),
+		div({ class: "market-card right-col" },
+		  renderCardField(`${i18n.marketItemStock}:`, item.stock > 0 ? item.stock : i18n.marketOutOfStock),
+		  div({ class: "market-card price" },
+		    renderCardField(`${i18n.marketItemPrice}:`, `${item.price} ECO`),
+		  ),
+		  renderCardField(`${i18n.marketItemCondition}:`, item.item_status),
+		  renderCardField(`${i18n.marketItemIncludesShipping}:`, item.includesShipping ? i18n.YESLabel : i18n.NOLabel),
+		  renderCardField(`${i18n.marketItemSeller}:`),
+		  div({ class: 'card-field' },
+		    a({ class: 'user-link', href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)
+		  ),
 		  item.item_type === 'auction' && item.auctions_poll.length > 0
 		  ? div({ class: "auction-info" },
 		      p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
 		      table({ class: 'auction-bid-table' },
-			tr(
-			  th(i18n.marketAuctionBidTime),
-			  th(i18n.marketAuctionUser),
-			  th(i18n.marketAuctionBidAmount)
-			),
-			item.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`)
-			  );
-			})
+		        tr(
+		          th(i18n.marketAuctionBidTime),
+		          th(i18n.marketAuctionUser),
+		          th(i18n.marketAuctionBidAmount)
+		        ),
+		        item.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,
-                  p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)),
-                  div({ class: "market-card buttons" },
-                    (filter === 'mine') ? [
-                      form({ method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` },
-                        button({ class: "delete-btn", type: "submit" }, i18n.marketActionsDelete)
-                      ),
-                      (item.status !== 'SOLD' && item.status !== 'DISCARDED' && item.auctions_poll.length === 0) 
-                        ? form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` },
-                        button({ class: "update-btn", type: "submit" }, i18n.marketActionsUpdate)
-                      )
-                        : null,
-                      (item.status === 'FOR SALE') 
-                        ? form({ method: "POST", action: `/market/sold/${encodeURIComponent(item.id)}` },
-                        button({ class: "sold-btn", type: "submit" }, i18n.marketActionsSold)
-                      )
-                        : null
-                    ] : [
-                      (item.status !== 'SOLD' && item.status !== 'DISCARDED')
-                        ? (item.item_type === 'auction'
-                          ? form({ method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` },
-                              input({ type: "number", name: "bidAmount", step:"0.000001", min:"0.000001", placeholder: i18n.marketYourBid, required: true }),
-                              br,
-                              button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
-                            )
-                          : form({ method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` },
-                              input({ type: "hidden", name: "buyerId", value: userId }),
-                              button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
-                            )
-                        )
-                        : null
-                    ]
-                  )
-                )
-              )
-            )
-          : p(i18n.marketNoItems)
-        )
+		  div({ class: "market-card buttons" },
+		    (filter === 'mine') ? [
+		      form({ method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` },
+		        button({ class: "delete-btn", type: "submit" }, i18n.marketActionsDelete)
+		      ),
+		      (item.status !== 'SOLD' && item.status !== 'DISCARDED' && item.auctions_poll.length === 0) 
+		        ? form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` },
+		        button({ class: "update-btn", type: "submit" }, i18n.marketActionsUpdate)
+		      )
+		        : null,
+		      (item.status === 'FOR SALE') 
+		        ? form({ method: "POST", action: `/market/sold/${encodeURIComponent(item.id)}` },
+		        button({ class: "sold-btn", type: "submit" }, i18n.marketActionsSold)
+		      )
+		        : null
+		    ] : [
+		      (item.status !== 'SOLD' && item.status !== 'DISCARDED')
+		        ? (item.item_type === 'auction'
+		          ? form({ method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` },
+		              input({ type: "number", name: "bidAmount", step:"0.000001", min:"0.000001", placeholder: i18n.marketYourBid, required: true }),
+		              br,
+		              button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
+		            )
+		          : form({ method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` },
+		              input({ type: "hidden", name: "buyerId", value: userId }),
+		              button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
+		            )
+		        )
+		        : null
+		    ]
+		  )
+		)
+	      )
+	    )
+	  : p(i18n.marketNoItems)
+	)
       )
     )
   );
@@ -257,26 +278,33 @@ exports.singleMarketView = async (item, filter) => {
       ),
       div({ class: "tags-header" },
         h2(item.title),
-        p(item.description),
-        p(`${i18n.marketItemType}: ${item.item_type}`),
-        p(`${i18n.marketItemCondition}: ${item.item_status}`),
-        p(`${i18n.marketItemPrice}: ${item.price} ECO`),
-        p(`${i18n.marketItemIncludesShipping}: ${item.includesShipping ? i18n.YESLabel : i18n.NOLabel}`),
-        p(`${i18n.marketItemStatus}: ${item.status}`),
-        item.deadline ? p(`${i18n.marketItemAvailable}: ${moment(item.deadline).format('YYYY/MM/DD HH:mm:ss')}`) : null,
-        p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)),
+        renderCardField(`${i18n.marketItemType}:`, `${item.item_type.toUpperCase()}`),
+        renderCardField(`${i18n.marketItemStatus}:`, item.status),
+        renderCardField(`${i18n.marketItemStock}:`, item.stock > 0 ? item.stock : i18n.marketOutOfStock),
+        renderCardField(`${i18n.marketItemCondition}:`, item.item_status),
+        renderCardField(`${i18n.marketItemPrice}:`, `${item.price} ECO`),
+        renderCardField(`${i18n.marketItemIncludesShipping}:`, `${item.includesShipping ? i18n.YESLabel : i18n.NOLabel}`),
+        item.deadline ? renderCardField(`${i18n.marketItemAvailable}:`, `${moment(item.deadline).format('YYYY/MM/DD HH:mm:ss')}`) : null,
+        br,
         div({ class: "market-item image" },
           item.image
             ? img({ src: `/blob/${encodeURIComponent(item.image)}` })
             : img({ src: '/assets/images/default-market.png', alt: item.title })
         ),
+        renderCardField(`${i18n.marketItemDescription}:`, item.description),
+        br,
         item.tags && item.tags.length
-          ? div(
+          ? div({ class: 'card-tags' },
               item.tags.map(tag =>
                 a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
               )
             )
-          : null
+          : null,
+          br,
+	div({ class: 'card-field' },
+	  span({ class: 'card-label' }, `${i18n.marketItemSeller}:`),
+	  a({ class: 'user-link', href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)
+	)
       ),
       item.item_type === 'auction' 
         ? div({ class: "auction-info" },

+ 194 - 186
src/views/opinions_view.js

@@ -1,4 +1,4 @@
-const { div, h2, p, section, button, form, a, img, video: videoHyperaxe, audio: audioHyperaxe, input, table, tr, th, td, br } = require("../server/node_modules/hyperaxe");
+const { div, h2, p, section, button, form, a, img, video: videoHyperaxe, audio: audioHyperaxe, input, table, tr, th, td, br, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const { config } = require('../server/SSB_server.js');
 
@@ -13,202 +13,208 @@ const generateFilterButtons = (filters, currentFilter) => {
 
 const renderContentHtml = (content, key) => {
   switch (content.type) {
-    case 'event':
-      return div({ class: 'opinion-event' },
-        form({ method: "GET", action: `/events/${encodeURIComponent(key)}` },
+  case 'bookmark':
+  return div({ class: 'opinion-bookmark' },
+    div({ class: 'card-section bookmark' },
+      form({ method: "GET", action: `/bookmarks/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(content.title),
-        content.description ? p(`${i18n.description}: ${content.description}`) : "",
-        content.date ? p(`${i18n.date}: ${new Date(content.date).toLocaleString()}`) : "",
-        content.location ? p(`${i18n.location}: ${content.location}`) : "",
-        typeof content.isPublic === 'boolean' ? p(`${i18n.isPublic || 'Public'}: ${content.isPublic ? 'Yes' : 'No'}`) : "",
-        content.status ? p(`${i18n.status}: ${content.status}`) : "",
-        content.price ? p(`${i18n.trendingPrice}: ${content.price} ECO`) : "",
-        content.url ? p(`${i18n.trendingUrl}: `, a({ href: content.url, target: '_blank' }, content.url)) : "",
-        content.organizer ? p(`${i18n.organizer || 'Organizer'}: `, a({ href: `/author/${encodeURIComponent(content.organizer)}` }, content.organizer)) : "",
-        Array.isArray(content.attendees) ? p(`${i18n.attendees}: ${content.attendees.length}`) : "",
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
-    case 'bookmark':
-      return div({ class: 'opinion-bookmark' },
-        form({ method: "GET", action: `/bookmarks/${encodeURIComponent(key)}` },
-        button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),br,
-        content.description ? p(content.description) : "",
-        h2(content.url ? p(a({ href: content.url, target: '_blank', class: "bookmark-url" }, content.url)) : ""),
-        content.category ? p(`${i18n.category}: ${content.category}`) : "",
-        content.lastVisit ? p(`${i18n.bookmarkLastVisit}: ${new Date(content.lastVisit).toLocaleString()}`) : "",
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
-    case 'task':
-      return div({ class: 'opinion-task' },
-        form({ method: "GET", action: `/tasks/${encodeURIComponent(key)}` },
+      ),
+      br,
+      content.description ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ':'),
+        span({ class: 'card-value' }, content.description)
+      ) : "",
+      h2(content.url ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, p(a({ href: content.url, target: '_blank', class: "bookmark-url" }, content.url)))
+      ) : ""),
+      content.lastVisit ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'),
+        span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())
+      ) : ""
+    )
+  );
+  case 'image':
+  return div({ class: 'opinion-image' },
+    div({ class: 'card-section image' },
+      form({ method: "GET", action: `/images/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(content.title),
-        content.description ? p(`${i18n.description}: ${content.description}`) : "",
-        content.startTime ? p(`${i18n.trendingStart}: ${new Date(content.startTime).toLocaleString()}`) : "",
-        content.endTime ? p(`${i18n.trendingEnd}: ${new Date(content.endTime).toLocaleString()}`) : "",
-        content.priority ? p(`${i18n.trendingPriority}: ${content.priority}`) : "",
-        content.location ? p(`${i18n.trendingLocation}: ${content.location}`) : "",
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null,
-        typeof content.isPublic === 'boolean' ? p(`${i18n.trendingIsPublic || 'Public'}: ${content.isPublic ? 'Yes' : 'No'}`) : "",
-        Array.isArray(content.assignees) ? p(`${i18n.trendingAssignees || 'Assignees'}: ${content.assignees.length}`) : "",
-        content.status ? p(`${i18n.trendingStatus}: ${content.status}`) : "",
-        content.author ? p(`${i18n.trendingAuthor || 'Author'}: `, a({ href: `/author/${encodeURIComponent(content.author)}` }, content.author)) : ""
-      );
-    case 'image':
-      return div({ class: 'opinion-image' },
-        form({ method: "GET", action: `/images/${encodeURIComponent(key)}` },
+      ),
+      br,
+      content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : "",
+      content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : "",
+      content.meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : "",
+      br,
+      div({ class: 'card-field' }, img({ src: `/blob/${encodeURIComponent(content.url)}`, class: 'feed-image' }))
+    )
+  );
+  case 'video':
+  return div({ class: 'opinion-video' },
+    div({ class: 'card-section video' },
+      form({ method: "GET", action: `/videos/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        content.title ? h2(content.title) : "",
-        content.description ? p(content.description) : "",
-        content.meme ? h2(`${i18n.category}: ${i18n.meme}`) : "",
-        img({ src: `/blob/${encodeURIComponent(content.url)}`, class: 'feed-image' }),
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
-    case 'video':
-      return div({ class: 'opinion-video' },
-        form({ method: "GET", action: `/videos/${encodeURIComponent(key)}` },
+      ),
+      br,
+      content.title ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.videoTitleLabel + ':'),
+        span({ class: 'card-value' }, content.title)
+      ) : "",
+      content.description ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'),
+        span({ class: 'card-value' }, content.description)
+      ) : "",
+      br,
+      div({ class: 'card-field' },
+        videoHyperaxe({
+          controls: true,
+          src: `/blob/${encodeURIComponent(content.url)}`,
+          type: content.mimeType || 'video/mp4',
+          width: '640',
+          height: '360'
+        })
+      )
+    )
+  );
+  case 'audio':
+  return div({ class: 'opinion-audio' },
+    div({ class: 'card-section audio' },
+      form({ method: "GET", action: `/audios/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        content.title ? h2(content.title) : "",
-        content.description ? p(content.description) : "",
-        videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360' }),
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
-    case 'audio':
-      return div({ class: 'opinion-audio' },
-        form({ method: "GET", action: `/audios/${encodeURIComponent(key)}` },
+      ),
+      br,
+      content.title ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.audioTitleLabel + ':'),
+        span({ class: 'card-value' }, content.title)
+      ) : "",
+      content.description ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'),
+        span({ class: 'card-value' }, content.description)
+      ) : "",
+      br,
+      div({ class: 'card-field' },
+        audioHyperaxe({
+          controls: true,
+          src: `/blob/${encodeURIComponent(content.url)}`,
+          type: content.mimeType,
+          preload: 'metadata'
+        })
+      )
+    )
+  );
+  case 'document':
+  return div({ class: 'opinion-document' },
+    div({ class: 'card-section document' },
+      form({ method: "GET", action: `/documents/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        content.title ? h2(content.title) : "",
-        content.description ? p(content.description) : "",
-        audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' }),
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
-    case 'document':
-      return div({ class: 'opinion-document' },
-        form({ method: "GET", action: `/documents/${encodeURIComponent(key)}` },
+      ),
+      br,
+      content.title ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.documentTitleLabel + ':'),
+        span({ class: 'card-value' }, content.title)
+      ) : "",
+      content.description ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.documentDescriptionLabel + ':'),
+        span({ class: 'card-value' }, content.description)
+      ) : "",
+      br,
+      div({ class: 'card-field' },
+        div({ class: 'pdf-viewer-container', 'data-pdf-url': `/blob/${encodeURIComponent(content.url)}` })
+      )
+    )
+  );
+  case 'feed':
+  return div({ class: 'opinion-feed' },
+    div({ class: 'card-section feed' },
+      h2(content.text),
+      h2({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.tribeFeedRefeeds}: `),
+        span({ class: 'card-value' }, content.refeeds)
+      )
+    )
+  );
+  case 'votes':
+  const votesList = content.votes && typeof content.votes === 'object'
+    ? Object.entries(content.votes).map(([option, count]) => ({ option, count }))
+    : [];
+  return div({ class: 'opinion-votes' },
+    div({ class: 'card-section votes' },
+      form({ method: "GET", action: `/votes/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        br,
-        content.title ? h2(content.title) : "",
-        content.description ? p(content.description) : "",
-        div({ class: 'pdf-viewer-container', 'data-pdf-url': `/blob/${encodeURIComponent(content.url)}` }),
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
-    case 'feed':
-      return div({ class: 'opinion-feed' },
-        h2(content.text),
-        p(`${i18n.author}: `, a({ href: `/author/${encodeURIComponent(content.author)}`, target: "_blank" }, content.author)),
-        p(`${i18n.createdAt}: ${new Date(content.createdAt).toLocaleString()}`),
-        h2(`${i18n.tribeFeedRefeeds}: ${content.refeeds}`)
-      );
-    case 'votes':
-      const votesList = content.votes && typeof content.votes === 'object'
-        ? Object.entries(content.votes).map(([option, count]) => ({ option, count }))
-        : [];
-      return div({ class: 'opinion-votes' },
-        form({ method: "GET", action: `/votes/${encodeURIComponent(key)}` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(content.question),
-        p(`${i18n.deadline}: ${content.deadline ? new Date(content.deadline).toLocaleString() : ''}`),
-        h2(`${i18n.voteTotalVotes}: ${content.totalVotes}`),
-        table(
-          tr(...votesList.map(({ option }) => th(i18n[option] || option))),
-          tr(...votesList.map(({ count }) => td(count)))
-        )
-      );
-  case 'report':
-    return div({ class: 'opinion-report' },
-      form({ method: "GET", action: `/reports/${encodeURIComponent(key)}` },
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.voteQuestionLabel + ':'),
+        span({ class: 'card-value' }, content.question)
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.voteDeadline + ':'),
+        span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '')
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.voteTotalVotes + ':'),
+        span({ class: 'card-value' }, content.totalVotes)
+      ),
+      table(
+        tr(...votesList.map(({ option }) => th(i18n[option] || option))),
+        tr(...votesList.map(({ count }) => td(count)))
+      )
+    )
+  );
+  case 'transfer':
+  return div({ class: 'opinion-transfer' },
+    div({ class: 'card-section transfer' },
+      form({ method: "GET", action: `/transfers/${encodeURIComponent(key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
       ),
       br,
-      p(`${i18n.title}: ${content.title}`),
-      content.description ? p(`${i18n.description}: ${content.description}`) : "",
-      content.category ? p(`${i18n.category}: ${content.category}`) : "",
-      content.severity ? p(`${i18n.severity || 'Severity'}: ${content.severity}`) : "",
-      content.status ? p(`${i18n.status}: ${content.status}`) : "",
-      content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}`, class: 'feed-image' }) : "",
-      content.createdAt ? p(`${i18n.date}: ${new Date(content.createdAt).toLocaleString()}`) : "",
-      typeof content.isAnonymous === 'boolean' 
-        ? p(`${i18n.author || 'Author'}: `, content.isAnonymous 
-        ? i18n.reportsAnonymousAuthor || 'Anonymous' 
-        : a({ href: `/author/${encodeURIComponent(content.author)}`, target: '_blank' }, content.author))
-        : content.author ? p(`${i18n.author || 'Author'}: `, a({ href: `/author/${encodeURIComponent(content.author)}`, target: '_blank' }, content.author)) : "",
-      Array.isArray(content.confirmations) ? p(`${i18n.confirmations || 'Confirmations'}: ${content.confirmations.length}`) : "",
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.concept + ':'),
+        span({ class: 'card-value' }, content.concept)
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.deadline + ':'),
+        span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '')
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.status + ':'),
+        span({ class: 'card-value' }, content.status)
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.amount + ':'),
+        span({ class: 'card-value' }, content.amount)
+      ),
+      br,
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.from + ':'),
+        span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(content.from)}`, target: "_blank" }, content.from))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.to + ':'),
+        span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(content.to)}`, target: "_blank" }, content.to))
+      ),
       br,
-      content.tags?.length
-        ? div(content.tags.map(tag =>
-           a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-          ))
-        : null
-    );
-    case 'transfer':
-      return div({ class: 'opinion-transfer' },
-        form({ method: "GET", action: `/transfers/${encodeURIComponent(key)}` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(`${i18n.concept}: ${content.concept}`),
-        p(`${i18n.from}: `, a({ href: `/author/${encodeURIComponent(content.from)}`, target: "_blank" }, content.from)),
-        p(`${i18n.to}: `, a({ href: `/author/${encodeURIComponent(content.to)}`, target: "_blank" }, content.to)),
-        h2(`${i18n.amount}: ${content.amount}`),
-        p(`${i18n.deadline}: ${content.deadline ? new Date(content.deadline).toLocaleString() : ""}`),
-        p(`${i18n.status}: ${content.status}`),
-        p(`${i18n.transfersConfirmations}: ${content.confirmedBy.length}/2`),
-        br,
-        content.tags?.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
-      );
+      div({ class: 'card-field' },
+        h2({ class: 'card-label' }, i18n.transfersConfirmations + ': ' + `${content.confirmedBy.length}/2`)
+      )
+    )
+  );
     default:
-      return div({ class: 'styled-text', innerHTML: content.text || content.description || content.title || '[no content]' });
+	return div({ class: 'styled-text' },
+	  div({ class: 'card-section styled-text-content' },
+	    div({ class: 'card-field' },
+	      span({ class: 'card-label' }, i18n.textContentLabel + ':'),
+	      span({ class: 'card-value', innerHTML: content.text || content.description || content.title || '[no content]' })
+	    )
+	  )
+	);
   }
 };
 
 exports.opinionsView = (items, filter) => {
+  items = items.filter(item => {
+    const content = item.value?.content || item.content;
+    if (!content || typeof content !== 'object') return false;
+    if (content.type === 'tombstone') return false;
+    return true;
+  });
   const title = i18n.opinionsTitle;
   const baseFilters = ['RECENT', 'ALL', 'MINE', 'TOP'];
   const categoryFilters = [
@@ -264,12 +270,14 @@ exports.opinionsView = (items, filter) => {
                 const key = item.key;
                 const contentHtml = renderContentHtml(c, key);
 
-                return div({ class: 'opinion-card' },
+                return div(
                   contentHtml,
-                  p(`${i18n.author}: `, a({ href: `/author/${encodeURIComponent(item.value.author)}` }, item.value.author)),
-                  p(`${i18n.createdAtLabel || i18n.opinionsCreatedAt}: ${created}`),
+                  p({ class: 'card-footer' },
+     		    span({ class: 'date-link' }, `${created} ${i18n.performed} `),
+     		    a({ href: `/author/${encodeURIComponent(item.value.author)}`, class: 'user-link' }, `${item.value.author}`)
+                  ), 
                   h2(`${i18n.totalOpinions || i18n.opinionsTotalCount}: ${total}`),
-		!voted
+		  !voted
 		  ? div({ class: 'voting-buttons' },
 		      ['interesting','necessary','funny','disgusting','sensible','propaganda','adultOnly','boring','confusing','inspiring','spam'].map(cat => 
 			form({

+ 0 - 1
src/views/peers_view.js

@@ -25,7 +25,6 @@ const peersView = async ({ peers, connectedPeers }) => {
       div({ class: "peers-list" },
         h2(`${i18n.online} (${connectedPeers.length})`),
         connectedPeers.length > 0 ? ul(renderPeerList(connectedPeers)) : p(i18n.noConnections),
-        br(),
         h2(`${i18n.offline} (${peers.length})`),
         peers.length > 0 ? ul(renderPeerList(peers)) : p(i18n.noDiscovered),
         p(i18n.connectionActionIntro)

+ 1 - 1
src/views/pixelia_view.js

@@ -84,7 +84,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => {
           h2(i18n.contributorsTitle),
           ul(
             ...contributors.map(author =>
-              li(a({ href: `/author/${encodeURIComponent(author)}` }, author))
+              li(a({ class: 'user-link', href: `/author/${encodeURIComponent(author)}` }, author))
             )
           )
         ) : null 

+ 83 - 72
src/views/report_view.js

@@ -5,61 +5,65 @@ const moment = require('../server/node_modules/moment');
 
 const userId = config.keys.id;
 
-const generateReportActions = (report, userId) => {
-  return report.author === userId
-    ? div({ class: "report-actions" },
-        form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` },
-          button({ type: "submit", class: "update-btn" }, i18n.reportsUpdateButton)
-        ),
-        form({ method: "POST", action: `/reports/delete/${encodeURIComponent(report.id)}` },
-          button({ type: "submit", class: "delete-btn" }, i18n.reportsDeleteButton)
-        ),
-        form({ method: "POST", action: `/reports/status/${encodeURIComponent(report.id)}` },
-          button({ type: "submit", name: "status", value: "OPEN" }, i18n.reportsStatusOpen), br(),
-          button({ type: "submit", name: "status", value: "UNDER_REVIEW" }, i18n.reportsStatusUnderReview), br(),
-          button({ type: "submit", name: "status", value: "RESOLVED" }, i18n.reportsStatusResolved), br(),
-          button({ type: "submit", name: "status", value: "INVALID" }, i18n.reportsStatusInvalid)
-        )
-      )
-    : null;
-};
+const renderCardField = (labelText, value) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, value)
+  );
+
+const renderReportCard = (report, userId) => {
+  const actions = report.author === userId ? [
+    form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` },
+      button({ type: "submit", class: "update-btn" }, i18n.reportsUpdateButton)
+    ),
+    form({ method: "POST", action: `/reports/delete/${encodeURIComponent(report.id)}` },
+      button({ type: "submit", class: "delete-btn" }, i18n.reportsDeleteButton)
+    ),
+    form({ method: "POST", action: `/reports/status/${encodeURIComponent(report.id)}` },
+      button({ type: "submit", name: "status", value: "OPEN" }, i18n.reportsStatusOpen), br(),
+      button({ type: "submit", name: "status", value: "UNDER_REVIEW" }, i18n.reportsStatusUnderReview), br(),
+      button({ type: "submit", name: "status", value: "RESOLVED" }, i18n.reportsStatusResolved), br(),
+      button({ type: "submit", name: "status", value: "INVALID" }, i18n.reportsStatusInvalid)
+    )
+  ] : [];
 
-const generateReportCard = (report, userId) => {
-  return div({ class: "report-item" },
-    generateReportActions(report, userId),
-    h2(report.title),
+  return div({ class: "card card-section report" },
+    actions.length ? div({ class: "report-actions" }, ...actions) : null,
     form({ method: 'GET', action: `/reports/${encodeURIComponent(report.id)}` },
       button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails)
     ),
-    p(`${i18n.reportsCategory}: ${report.category}`),
-    p(`${i18n.reportsSeverity}: ${report.severity}`),
-    p(`${i18n.reportsStatus}: ${report.status}`),
-    p(`${i18n.reportsDescriptionLabel}: ${report.description}`),
-    report.image ? img({ src: `/blob/${encodeURIComponent(report.image)}`, class: "report-image" }) : p(i18n.reportsNoFile),
-    p(`${i18n.reportsCreatedAt}: ${moment(report.createdAt).format('YYYY-MM-DD HH:mm')}`),
-    p(`${i18n.reportsCreatedBy}: `,
-      report.isAnonymous
-        ? span({ class: "anonymous-label" }, i18n.reportsAnonymousAuthor)
-        : a({ href: `/author/${encodeURIComponent(report.author)}` }, report.author)
+    br,
+    renderCardField(i18n.reportsTitleLabel + ":", report.title),
+    renderCardField(i18n.reportsStatus + ":", report.status),
+    renderCardField(i18n.reportsSeverity + ":", report.severity.toUpperCase()),
+    renderCardField(i18n.reportsCategory + ":", report.category),
+    renderCardField(i18n.reportsConfirmations + ":", report.confirmations.length),
+    renderCardField(i18n.reportsDescriptionLabel + ":", report.description),
+    br,
+    div({ class: 'card-field' },
+      report.image ? div({ class: 'card-field' },
+        img({ src: `/blob/${encodeURIComponent(report.image)}`, class: "report-image" }),
+      ) : null
     ),
-    p(`${i18n.reportsConfirmations}: ${report.confirmations.length}`),
+    br,
     form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` },
       button({ type: "submit" }, i18n.reportsConfirmButton)
     ),
-
-    a({ href: "/tasks?filter=create", target: "_blank" }, button({ type: "button" }, i18n.reportsCreateTaskButton)), br(), br(),
+    a({ href: "/tasks?filter=create", target: "_blank" },
+      button({ type: "button" }, i18n.reportsCreateTaskButton)
+    ),
+    br(), br(),
     report.tags && report.tags.length
-      ? div(
+      ? div({ class: "card-tags" },
           report.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right:0.8em;" }, `#${tag}`)
+            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
           )
-        ) : null,
-    div({ class: "voting-buttons" },
-      ["interesting", "necessary", "funny", "disgusting", "sensible", "propaganda", "adultOnly", "boring", "confusing", "inspiring", "spam"].map(category =>
-        form({ method: "POST", action: `/reports/opinions/${encodeURIComponent(report.id)}/${category}` },
-          button({ class: "vote-btn" }, `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${report.opinions?.[category] || 0}]`)
         )
-      )
+      : null,
+    br,
+    p({ class: 'card-footer' },
+      span({ class: 'date-link' }, `${moment(report.createdAt).format('YYYY-MM-DD HH:mm')} ${i18n.performed} `),
+        a({ class: "user-link", href: `/author/${encodeURIComponent(report.author)}`, class: 'user-link' }, report.author)
     )
   );
 };
@@ -128,10 +132,10 @@ exports.reportView = async (reports, filter, reportId) => {
 
               label(i18n.reportsSeverity), br(),
               select({ name: "severity" },
-                option({ value: "low", selected: reportToEdit?.severity === 'low' }, i18n.reportsSeverityLow),
-                option({ value: "medium", selected: reportToEdit?.severity === 'medium' }, i18n.reportsSeverityMedium),
+                option({ value: "critical", selected: reportToEdit?.severity === 'critical' }, i18n.reportsSeverityCritical),
                 option({ value: "high", selected: reportToEdit?.severity === 'high' }, i18n.reportsSeverityHigh),
-                option({ value: "critical", selected: reportToEdit?.severity === 'critical' }, i18n.reportsSeverityCritical)
+                option({ value: "medium", selected: reportToEdit?.severity === 'medium' }, i18n.reportsSeverityMedium),
+                option({ value: "low", selected: reportToEdit?.severity === 'low' }, i18n.reportsSeverityLow)
               ), br(), br(),
 
               label(i18n.reportsUploadFile), br(),
@@ -140,14 +144,11 @@ exports.reportView = async (reports, filter, reportId) => {
               label("Tags"), br(),
               input({ type: "text", name: "tags", value: reportToEdit?.tags?.join(', ') || '' }), br(), br(),
 
-              label(i18n.reportsAnonymityOption),
-              input({ type: "checkbox", name: "isAnonymous", checked: reportToEdit?.isAnonymous || false }), br(), br(),
-
               button({ type: "submit" }, filter === 'edit' ? i18n.reportsUpdateButton : i18n.reportsCreateButton)
             )
           )
         : div({ class: "report-list" },
-            filtered.length > 0 ? filtered.map(r => generateReportCard(r, userId)) : p(i18n.reportsNoItems)
+            filtered.length > 0 ? filtered.map(r => renderReportCard(r, userId)) : p(i18n.reportsNoItems)
        )
      )
   );
@@ -173,33 +174,43 @@ exports.singleReportView = async (report, filter) => {
           button({ type: 'submit', name: 'filter', value: 'create', class: "create-button" }, i18n.reportsCreateButton)
         )
       ),
-      div({ class: "tags-header" },
-        h2(report.title),
-        p(report.description),
-        p(`${i18n.reportsCategory}: ${report.category}`),
-        p(`${i18n.reportsSeverity}: ${report.severity}`),
-        p(`${i18n.reportsStatus}: ${report.status}`),
-        report.image ? img({ src: `/blob/${encodeURIComponent(report.image)}`, class: "report-image" }) : p(i18n.reportsNoFile),
-        p(`${i18n.reportsCreatedAt}: ${moment(report.createdAt).format('YYYY-MM-DD HH:mm')}`),
-        p(`${i18n.reportsCreatedBy}: `,
-          report.isAnonymous
-            ? span({ class: "anonymous-label" }, i18n.reportsAnonymousAuthor)
-            : a({ href: `/author/${encodeURIComponent(report.author)}` }, report.author)
+      div({ class: "card card-section report" },
+        form({ method: 'GET', action: `/reports/${encodeURIComponent(report.id)}` },
+          button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails)
+        ),
+        br,
+        renderCardField(i18n.reportsTitleLabel + ":", report.title),
+        renderCardField(i18n.reportsStatus + ":", report.status),
+        renderCardField(i18n.reportsSeverity + ":", report.severity.toUpperCase()),
+        renderCardField(i18n.reportsCategory + ":", report.category),
+        renderCardField(i18n.reportsConfirmations + ":", report.confirmations.length),
+        renderCardField(i18n.reportsDescriptionLabel + ":", report.description),
+        br,
+        div({ class: 'card-field' },
+          report.image ? div({ class: 'card-field' },
+            img({ src: `/blob/${encodeURIComponent(report.image)}`, class: "report-image" }),
+          ) : null
+        ),
+        br,
+        form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` },
+          button({ type: "submit" }, i18n.reportsConfirmButton)
         ),
-        p(`${i18n.reportsConfirmations}: ${report.confirmations.length}`),
+        a({ href: "/tasks?filter=create", target: "_blank" },
+          button({ type: "button" }, i18n.reportsCreateTaskButton)
+        ),
+        br(), br(),
         report.tags && report.tags.length
-          ? div(
+          ? div({ class: "card-tags" },
               report.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right:0.8em;" }, `#${tag}`)
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
               )
             )
-          : null
-      ),
-      div({ class: "report-actions" },
-        form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` },
-          button({ type: "submit" }, i18n.reportsConfirmButton)
-        ),
-        a({ href: "/tasks?filter=create", target: "_blank" }, button({ type: "button" }, i18n.reportsCreateTaskButton))
+          : null,
+        br,
+        p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(report.createdAt).format('YYYY-MM-DD HH:mm')} ${i18n.performed} `),
+            a({ class: "user-link", href: `/author/${encodeURIComponent(report.author)}`, class: 'user-link' }, report.author)
+        )
       )
     )
   );

+ 196 - 139
src/views/search_view.js

@@ -1,4 +1,4 @@
-const { form, button, div, h2, p, section, input, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, a } = require("../server/node_modules/hyperaxe");
+const { form, button, div, h2, p, section, input, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const moment = require("../server/node_modules/moment");
 const { renderTextWithStyles } = require('../backend/renderTextWithStyles');
@@ -45,141 +45,184 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
     option({ value: "10", selected: resultCount === "10" }, "10"),
     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 renderContentHtml = (content) => {
   switch (content.type) {
     case 'post':
       return div({ class: 'search-post' },
-        content.contentWarning ? p(i18n.contentWarning + `: ${content.contentWarning}`) : null,
-        content.text ? p({ innerHTML: content.text }) : null,
-        content.tags && content.tags.length
-           ? div(content.tags.map(tag =>
-           a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-         ))
-       : null
-    );
+        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 ? h2('@', content.name) : null,
-        content.description ? p(content.description) : null,
+        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(content.text) : null,
-        div(
-          h2(`${i18n.tribeFeedRefeeds}: ${content.refeeds}`)
-        )
-    );
+        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 ? h2(content.title) : null,
-        content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
-        content.date ? p(`${i18n.eventDate}: ${new Date(content.date).toLocaleString()}`) : null,
-        content.location ? p(`${i18n.eventLocation}: ${content.location}`) : null,
-        content.price ? p(`${i18n.eventPrice}: ${content.price} ECO`) : null,
-        content.eventUrl ? p(`${i18n.eventUrlLabel}: `, a({ href: content.eventUrl, target: '_blank' }, content.eventUrl)) : null,
-        content.organizer ? p(`${i18n.eventOrganizer}: `, a({ href: `/author/${encodeURIComponent(content.organizer)}` }, content.organizer)) : null,
+        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(content.tags.map(tag =>
+          ? 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' },
-        content.question ? h2(content.question) : null,
-        content.status ? p(`${i18n.voteStatus}: ${content.status}`) : null,
-        content.totalVotes ? p(`${i18n.voteTotalVotes}: ${content.totalVotes}`) : null,
-        content.votes && content.votes.YES ? p(`${i18n.voteYes}: ${content.votes.YES}`) : null,
-        content.votes && content.votes.NO ? p(`${i18n.voteNo}: ${content.votes.NO}`) : null,
-        content.votes && content.votes.ABSTENTION ? p(`${i18n.voteAbstention}: ${content.votes.ABSTENTION}`) : null,
-        content.votes && content.votes.FOLLOW_MAJORITY ? p(`${i18n.voteFollowMajority}: ${content.votes.FOLLOW_MAJORITY}`) : null,
-        content.tags && content.tags.length
-          ? div(content.tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-          ))
-        : null
+        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' },
-        h2(content.title),
-        content.description ? p(i18n.tribeDescriptionLabel + ': ' + content.description) : null,
+        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' }),
-        p(`${i18n.location || 'Location'}: ${content.location || ''}`),
-        typeof content.isLARP === 'boolean' ? p(`${i18n.isLARPLabel || 'LARP'}: ${content.isLARP ? 'Yes' : 'No'}`) : null,
-        typeof content.isAnonymous === 'boolean' ? p(`${i18n.isAnonymousLabel || 'Anonymous'}: ${content.isAnonymous ? 'Yes' : 'No'}`) : null,
-        Array.isArray(content.members) ? p(`${i18n.tribeMembersCount || 'Members'}: ${content.members.length}`) : null,
+        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(content.tags.map(tag =>
+          ? 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 ? h2(content.title) : null,
-        content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
+        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(content.tags.map(tag =>
+          ? 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 ? h2(content.title) : null,
-        content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
-        content.meme ? h2(`${i18n.trendingCategory}: ${i18n.meme}`) : null,
+        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(),
+        br,
         content.tags && content.tags.length
-          ? div(content.tags.map(tag =>
+          ? 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 ? h2(content.title) : null,
-        content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
+        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(content.tags.map(tag =>
+          ? 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 ? h2(content.title) : null,
-        content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
+        content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), 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,
+        br,
         div({
           id: `pdf-container-${content.key || content.url}`,
-          class: 'pdf-viewer-container',
-         'data-pdf-url': `/blob/${encodeURIComponent(content.url)}`
+          class: 'card-field pdf-viewer-container',
+          'data-pdf-url': `/blob/${encodeURIComponent(content.url)}`
         }),
-       content.tags && content.tags.length
-          ? div(content.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
-            ))
-          : null
-     );
+        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.item_type ? h2(`${i18n.marketItemType}: ${content.item_type}`) : null,
-        content.title ? h2(content.title) : null,
-        content.description ? p(`${i18n.searchDescription}: ${content.description}`) : null,
-        content.price ? p(`${i18n.searchPriceLabel}: ${content.price || 'N/A'}`) : null,
-        content.status ? p(`${i18n.marketItemCondition}: ${content.status}`) : null,
-        content.item_status ? p(`${i18n.marketItemCondition}: ${content.item_status}`) : null,
-        content.deadline ? p(`${i18n.marketItemDeadline}: ${new Date(content.deadline).toLocaleString()}`) : null,
-        typeof content.includesShipping === 'boolean' ? p(`${i18n.marketItemIncludesShipping}: ${content.includesShipping ? i18n.YESLabel : i18n.NOLabel}`) : null,
-        content.seller ? p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(content.seller)}` }, content.seller)) : null,
+        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),
@@ -201,105 +244,106 @@ const renderContentHtml = (content) => {
           )
           : null,
         content.tags && content.tags.length
-          ? div(content.tags.map(tag =>
+          ? 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 ? p(content.description) : null,
-        h2(content.url ? a({ href: content.url, target: '_blank' }, content.url) : null),
-        content.category ? p(`${i18n.bookmarkCategory}: ${content.category}`) : null,
-        content.lastVisit ? p(`${i18n.bookmarkLastVisit}: ${new Date(content.lastVisit).toLocaleString()}`) : null,
+        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(content.tags.map(tag =>
+          ? 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 ? h2(content.title) : null,
-       content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
-       content.startTime ? p(`${i18n.taskStartTimeLabel}: ${new Date(content.startTime).toLocaleString()}`) : null,
-       content.endTime ? p(`${i18n.taskEndTimeLabel}: ${new Date(content.endTime).toLocaleString()}`) : null,
-       content.priority ? p(`${i18n.searchPriorityLabel}: ${content.priority}`) : null,
-       content.location ? p(`${i18n.searchLocationLabel}: ${content.location}`) : null,
-       typeof content.isPublic === 'boolean' ? p(`${i18n.searchIsPublicLabel}: ${content.isPublic ? i18n.YESLabel : i18n.NOLabel}`) : null,
-       Array.isArray(content.assignees)
-          ? p(`${i18n.taskAssignees}: ${content.assignees.length}`)
-          : null,
-       content.status ? p(`${i18n.searchStatusLabel}: ${content.status}`) : null,
-       content.author ? p(`${i18n.author}: `, a({ href: `/author/${encodeURIComponent(content.author)}` }, content.author)) : null,
-       content.tags && content.tags.length
-         ? div(content.tags.map(tag =>
+        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':
+          ))
+        : null
+      );
+    case 'report':
       return div({ class: 'search-report' },
-      content.title ? h2(content.title) : null,
-      content.description ? p(i18n.searchDescription + `: ${content.description}`) : null,
-      content.category ? p(`${i18n.searchCategoryLabel}: ${content.category}`) : null,
-      content.severity ? p(`${i18n.reportsSeverity}: ${content.severity}`) : null,
-      content.status ? p(`${i18n.searchStatusLabel}: ${content.status}`) : null,
-      content.image ? img({ src: `/blob/${encodeURIComponent(content.image)}` }) : null,
-      typeof content.confirmations === 'number' ? p(`${i18n.reportsConfirmations}: ${content.confirmations}`) : null,
-      br,
-      content.tags && content.tags.length
-        ? div(content.tags.map(tag =>
+        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' },
-        p(`${i18n.transfersFrom}: `, a({ href: `/author/${encodeURIComponent(content.from)}` }, content.from)),
-        p(`${i18n.transfersTo}: `, a({ href: `/author/${encodeURIComponent(content.to)}` }, content.to)),
-        p(`${i18n.transfersAmount}: ${content.amount}`),
-        h2(`${i18n.transfersConcept}: ${content.concept}`),
-        p(`${i18n.transfersStatus}: ${content.status}`),
+        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
-          ? p(`${i18n.transfersConfirmations}: ${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(content.tags.map(tag =>
+          ? 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 ? h2(content.name) : null,
-        content.description ? p(content.description) : null,
+        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 ? p(`${i18n.cvLocationLabel}: ${content.location}`) : null,
-        content.status ? p(`${i18n.cvStatusLabel}: ${content.status}`) : null,
-        content.preferences ? p(`${i18n.cvPreferencesLabel}: ${content.preferences}`) : 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(content.personalSkills.map(skill =>
+          ? 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(content.personalExperiences.map(exp => p(exp))) : null,
+          ? div({ class: 'card-field' }, content.personalExperiences.map(exp => p(exp))) : null,
         Array.isArray(content.oasisExperiences) && content.oasisExperiences.length
-          ? div(content.oasisExperiences.map(exp => p(exp))) : null,
+          ? div({ class: 'card-field' }, content.oasisExperiences.map(exp => p(exp))) : null,
         Array.isArray(content.oasisSkills) && content.oasisSkills.length
-          ? div(content.oasisSkills.map(skill => p(skill))) : null,
+          ? div({ class: 'card-field' }, content.oasisSkills.map(skill => p(skill))) : null,
         Array.isArray(content.educationExperiences) && content.educationExperiences.length
-          ? div(content.educationExperiences.map(exp => p(exp))) : null,
+          ? div({ class: 'card-field' }, content.educationExperiences.map(exp => p(exp))) : null,
         Array.isArray(content.educationalSkills) && content.educationalSkills.length
-          ? div(content.educationalSkills.map(skill =>
+          ? 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(content.languages.map(lang => p(lang))) : null,
+          ? div({ class: 'card-field' }, content.languages.map(lang => p(lang))) : null,
         Array.isArray(content.professionalExperiences) && content.professionalExperiences.length
-          ? div(content.professionalExperiences.map(exp => p(exp))) : null,
+          ? div({ class: 'card-field' }, content.professionalExperiences.map(exp => p(exp))) : null,
        Array.isArray(content.professionalSkills) && content.professionalSkills.length
-          ? div(content.professionalSkills.map(skill => p(skill))) : null
+          ? 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]') });
@@ -318,7 +362,6 @@ const resultSection = Object.entries(results).length > 0
         const contentHtml = renderContentHtml(content);
         let author;
         let authorUrl = '#';
-
         if (content.type === 'market') {
           author = content.seller || i18n.anonymous || "Anonymous";
           authorUrl = `/author/${encodeURIComponent(content.seller)}`;
@@ -329,20 +372,34 @@ const resultSection = Object.entries(results).length > 0
           author = content.from || i18n.anonymous || "Anonymous";
           authorUrl = `/author/${encodeURIComponent(content.from)}`;
         } else if (content.type === 'post' || content.type === 'about') {
-          author = null;
-        } else if (content.type === 'report' && content.isAnonymous) {
-          author = null;
-        } else {
+          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)
+        );
 
         return div({ class: 'result-item' }, [
+          detailsButton,
+          br,
           contentHtml,
           author
-            ? p(`${i18n.author}: `, a({ href: authorUrl }, author))
-            : null,
-          p(`${i18n.createdAtLabel || i18n.searchCreatedAt}: ${created}`)
+            ? p({ class: 'card-footer' },
+             span({ class: 'date-link' }, `${created} ${i18n.performed} `),
+             a({ href: authorUrl, class: 'user-link' }, `${author}`)
+          ): null, 
+          
         ]);
       })
     )

+ 4 - 1
src/views/settings_view.js

@@ -95,7 +95,10 @@ const settingsView = ({ version }) => {
     section(
       div({ class: "tags-header" },
         h2(i18n.wallet),
-        p(i18n.walletSettingsDescription),
+	p(
+	  i18n.walletSettingsDescription, " ",
+	  a({ href: "docs/ecoin.md", target: "_blank", rel: "noopener" }, `[${i18n.walletSettingsDocLink}]`)
+	),
         form(
           { action: "/settings/wallet", method: "POST" },
           label({ for: "wallet_url" }, i18n.walletAddress), br(),

+ 2 - 2
src/views/stats_view.js

@@ -30,8 +30,8 @@ exports.statsView = (stats, filter) => {
       ),
       section(
 	div({ style: 'background-color:#f8f9fa; padding:24px; border-radius:8px; border:1px solid #e0e0e0; box-shadow:0 2px 8px rgba(0,0,0,0.1);' },
-	    h3({ style: 'font-size:18px; color:#555; margin:8px 0; font-weight:600;' }, `${i18n.statsOasisID}: `, a({ href: `/author/${encodeURIComponent(stats.id)}`, style: 'color:#007bff; text-decoration:none;' }, stats.id)),
-	    h3({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsCreatedAt}: `, span({ style: 'color:#888;' }, stats.createdAt)),
+	 h3({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsCreatedAt}: `, span({ style: 'color:#888;' }, stats.createdAt)),
+	  h3({ style: 'font-size:18px; color:#555; margin:8px 0; font-weight:600;' }, a({ class: "user-link", href: `/author/${encodeURIComponent(stats.id)}`, style: 'color:#007bff; text-decoration:none;' }, stats.id)),
 	  div({ style: 'margin-bottom:16px;' },
 	  ul({ style: 'list-style-type:none; padding:0; margin:0;' },
 	    li({ style: 'font-size:18px; color:#555; margin:8px 0;' },

+ 1 - 3
src/views/tags_view.js

@@ -4,13 +4,11 @@ const moment = require("../server/node_modules/moment");
 
 const getFilteredTags = (filter, tags) => {
   let filteredTags = tags.filter(t => !t.tombstone);
-
   if (filter === 'top') {
-    filteredTags = [...filteredTags].sort((a, b) => b.count - a.count);
+    filteredTags = filteredTags.sort((a, b) => b.count - a.count); 
   } else {
     filteredTags = filteredTags.sort((a, b) => a.name.localeCompare(b.name));
   }
-
   return filteredTags;
 };
 

+ 62 - 102
src/views/task_view.js

@@ -1,18 +1,15 @@
-const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label } = require("../server/node_modules/hyperaxe");
+const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label, span } = require("../server/node_modules/hyperaxe");
 const moment = require('../server/node_modules/moment');
 const { template, i18n } = require('./main_views');
 const { config } = require('../server/SSB_server.js');
 
 const userId = config.keys.id;
 
-const generateFilterButtons = (filters, currentFilter, action) => {
-  return filters.map(mode =>
-    form({ method: 'GET', action },
-      input({ type: 'hidden', name: 'filter', value: mode }),
-      button({ type: 'submit', class: currentFilter === mode.toLowerCase() ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
-    )
+const renderStyledField = (labelText, valueElement) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, valueElement)
   );
-};
 
 const renderTaskItem = (task, filter, userId) => {
   const actions = [];
@@ -29,38 +26,45 @@ const renderTaskItem = (task, filter, userId) => {
   }
   if (task.status !== 'CLOSED') {
     actions.push(
-      form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(task.id)}` }, button({ type: 'submit' }, task.assignees.includes(userId) ? i18n.taskUnassignButton : i18n.taskAssignButton))
+      form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(task.id)}` },
+        button({ type: 'submit' }, task.assignees.includes(userId) ? i18n.taskUnassignButton : i18n.taskAssignButton)
+      )
     );
   }
-  return div({ class: 'task-item' },
+  return div({ class: 'card card-section task' },
     actions.length > 0 ? div({ class: 'task-actions' }, ...actions) : null,
-    h2(task.title),
-    p(`${i18n.taskDescriptionLabel}: ${task.description}`),
-    p(`${i18n.taskStartTimeLabel}: ${moment(task.startTime).format(i18n.dateFormat)}`),
-    p(`${i18n.taskEndTimeLabel}: ${moment(task.endTime).format(i18n.dateFormat)}`),
-    p(`${i18n.taskPriorityLabel}: ${task.priority}`),
-    task.location?.trim() ? p(`${i18n.taskLocationLabel}: ${task.location}`) : null,
-    p(`${i18n.taskCreatedAt}: ${moment(task.createdAt).format(i18n.dateFormat)}`),
-    p(`${i18n.taskBy}: `, a({ href: `/author/${encodeURIComponent(task.author)}` }, task.author)),
-    p(`${i18n.taskStatus}: ${task.status}`),
-    p(`${i18n.taskVisibilityLabel}: ${task.isPublic}`),
-    p(`${i18n.taskAssignedTo}: `,
-      Array.isArray(task.assignees) && task.assignees.length
-        ? task.assignees.map((id, i) => [i > 0 ? ', ' : '', a({ href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
-        : i18n.noAssignees
+    form({ method: 'GET', action: `/tasks/${encodeURIComponent(task.id)}` }, button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails)),
+    br,
+    renderStyledField(i18n.taskTitleLabel + ':', task.title),
+    renderStyledField(i18n.taskDescriptionLabel + ':', task.description),
+    task.location?.trim() ? renderStyledField(i18n.taskLocationLabel + ':', task.location) : null,
+    renderStyledField(i18n.taskStatus + ':', task.status),
+    renderStyledField(i18n.taskPriorityLabel + ':', task.priority),
+    renderStyledField(i18n.taskVisibilityLabel + ':', task.isPublic),
+    renderStyledField(i18n.taskStartTimeLabel + ':', moment(task.startTime).format('YYYY/MM/DD HH:mm:ss')),
+    renderStyledField(i18n.taskEndTimeLabel + ':', moment(task.endTime).format('YYYY/MM/DD HH:mm:ss')),  
+    br,
+    div({ class: 'card-field' },
+      span({ class: 'card-label' }, i18n.taskAssignedTo + ':'),
+      span({ class: 'card-value' },
+        Array.isArray(task.assignees) && task.assignees.length
+          ? task.assignees.map((id, i) => [i > 0 ? ', ' : '', a({ class: "user-link", href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
+          : i18n.noAssignees
+      )
     ),
-    task.tags && task.tags.filter(Boolean).length
-      ? div(
-        task.tags.filter(Boolean).map(tag =>
-          a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link', style: 'margin-right:0.8em;margin-bottom:0.5em;' }, `#${tag}`)
+    br,
+    task.tags && task.tags.length
+      ? div({ class: 'card-tags' },
+          task.tags.map(tag =>
+            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+          )
         )
-      )
-      : null,
-    div({ class: 'voting-buttons' },
-      ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(cat =>
-        form({ method: 'POST', action: `/tasks/opinions/${encodeURIComponent(task.id)}/${cat}` }, button({ class: 'vote-btn' }, `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)]} [${task.opinions?.[cat] || 0}]`))
-      )
-    )
+      : null, 
+    br,
+    p({ class: 'card-footer' },
+      span({ class: 'date-link' }, `${moment(task.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+      a({ href: `/author/${encodeURIComponent(task.author)}`, class: 'user-link' }, `${task.author}`)
+    )   
   );
 };
 
@@ -153,56 +157,7 @@ exports.taskView = async (tasks, filter, taskId) => {
           )
         : div({ class: 'task-list' },
             filtered.length > 0
-              ? filtered.map(t => {
-                  const actions = [];
-                  if (filter === 'mine' && t.author === userId) {
-                    actions.push(
-                      form({ method: 'GET', action: `/tasks/edit/${encodeURIComponent(t.id)}` }, button({ type: 'submit', class: 'update-btn' }, i18n.taskUpdateButton)),
-                      form({ method: 'POST', action: `/tasks/delete/${encodeURIComponent(t.id)}` }, button({ type: 'submit', class: 'delete-btn' }, i18n.taskDeleteButton)),
-                      form({ method: 'POST', action: `/tasks/status/${encodeURIComponent(t.id)}` },
-                        button({ type: 'submit', name: 'status', value: 'OPEN' }, i18n.taskStatusOpen), br(),
-                        button({ type: 'submit', name: 'status', value: 'IN-PROGRESS' }, i18n.taskStatusInProgress), br(),
-                        button({ type: 'submit', name: 'status', value: 'CLOSED' }, i18n.taskStatusClosed)
-                      )
-                    );
-                  }
-                  if (t.status !== 'CLOSED') {
-                    actions.push(
-                      form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(t.id)}` }, button({ type: 'submit' }, t.assignees.includes(userId) ? i18n.taskUnassignButton : i18n.taskAssignButton))
-                    );
-                  }
-                  return div({ class: 'task-item' },
-                    actions.length > 0 ? div({ class: 'task-actions' }, ...actions) : null,
-                    form({ method: 'GET', action: `/tasks/${encodeURIComponent(t.id)}` }, button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails)),
-                    h2(t.title),
-                    p(`${i18n.taskDescriptionLabel}: ${t.description}`),
-                    p(`${i18n.taskStartTimeLabel}: ${moment(t.startTime).format(i18n.dateFormat)}`),
-                    p(`${i18n.taskEndTimeLabel}: ${moment(t.endTime).format(i18n.dateFormat)}`),
-                    p(`${i18n.taskPriorityLabel}: ${t.priority}`),
-                    t.location?.trim() ? p(`${i18n.taskLocationLabel}: ${t.location}`) : null,
-                    p(`${i18n.taskCreatedAt}: ${moment(t.createdAt).format(i18n.dateFormat)}`),
-                    p(`${i18n.taskBy}: `, a({ href: `/author/${encodeURIComponent(t.author)}` }, t.author)),
-                    p(`${i18n.taskStatus}: ${t.status}`),
-                    p(`${i18n.taskVisibilityLabel}: ${t.isPublic}`),
-                    p(`${i18n.taskAssignedTo}: `,
-                      Array.isArray(t.assignees) && t.assignees.length
-                        ? t.assignees.map((id, i) => [i > 0 ? ', ' : '', a({ href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
-                        : i18n.noAssignees
-                    ),
-                    t.tags && t.tags.filter(Boolean).length
-                      ? div(
-                          t.tags.filter(Boolean).map(tag =>
-                            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link', style: 'margin-right:0.8em;margin-bottom:0.5em;' }, `#${tag}`)
-                          )
-                        )
-                      : null,
-                    div({ class: 'voting-buttons' },
-                      ['interesting','necessary','funny','disgusting','sensible','propaganda','adultOnly','boring','confusing','inspiring','spam'].map(cat =>
-                        form({ method: 'POST', action: `/tasks/opinions/${encodeURIComponent(t.id)}/${cat}` }, button({ class: 'vote-btn' }, `${i18n['vote'+cat.charAt(0).toUpperCase()+cat.slice(1)]} [${t.opinions?.[cat]||0}]`))
-                      )
-                    ),
-                  );
-                })
+              ? filtered.map(t => renderTaskItem(t, filter, userId))
               : p(i18n.notasks)
           )
     )
@@ -228,25 +183,30 @@ exports.singleTaskView = async (task, filter) => {
           button({ type: 'submit', name: 'filter', value: 'create', class: "create-button" }, i18n.taskCreateButton)
         )
       ),
-      div({ class: "tags-header" },
-        h2(task.title),
-        p(task.description),
-        p(`${i18n.taskStartTimeLabel}: ${moment(task.startTime).format(i18n.dateFormat)}`),
-        p(`${i18n.taskEndTimeLabel}: ${moment(task.endTime).format(i18n.dateFormat)}`),
-        p(`${i18n.taskPriorityLabel}: ${task.priority}`),
-        task.location?.trim() ? p(`${i18n.taskLocationLabel}: ${task.location}`) : null,
-        p(`${i18n.taskCreatedAt}: ${moment(task.createdAt).format(i18n.dateFormat)}`),
-        p(`${i18n.taskBy}: `, a({ href: `/author/${encodeURIComponent(task.author)}` }, task.author)),
-        p(`${i18n.taskStatus}: ${task.status}`),
-        p(`${i18n.taskVisibilityLabel}: ${task.isPublic}`),
-        p(`${i18n.taskAssignedTo}: `,
-          Array.isArray(task.assignees) && task.assignees.length
-            ? task.assignees.map((id, i) => [i > 0 ? ', ' : '', a({ href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
-            : i18n.noAssignees
+      div({ class: 'card card-section task' },
+        renderStyledField(i18n.taskTitleLabel + ':', task.title),
+        renderStyledField(i18n.taskDescriptionLabel + ':', task.description),
+        renderStyledField(i18n.taskStartTimeLabel + ':', moment(task.startTime).format('YYYY/MM/DD HH:mm:ss')),
+        renderStyledField(i18n.taskEndTimeLabel + ':', moment(task.endTime).format('YYYY/MM/DD HH:mm:ss')),
+        renderStyledField(i18n.taskPriorityLabel + ':', task.priority),
+        task.location?.trim() ? renderStyledField(i18n.taskLocationLabel + ':', task.location) : null,
+        renderStyledField(i18n.taskCreatedAt + ':', moment(task.createdAt).format(i18n.dateFormat)),
+        renderStyledField(i18n.taskBy + ':', a({ href: `/author/${encodeURIComponent(task.author)}` }, task.author)),
+        renderStyledField(i18n.taskStatus + ':', task.status),
+        renderStyledField(i18n.taskVisibilityLabel + ':', task.isPublic),
+        div({ class: 'card-field' },
+          span({ class: 'card-label' }, i18n.taskAssignedTo + ':'),
+          span({ class: 'card-value' },
+            Array.isArray(task.assignees) && task.assignees.length
+              ? task.assignees.map((id, i) => [i > 0 ? ', ' : '', a({ href: `/author/${encodeURIComponent(id)}` }, id)]).flat()
+              : i18n.noAssignees
+          )
         ),
         task.tags && task.tags.length
-          ? div(
-              task.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))
+          ? div({ class: 'card-tags' },
+              task.tags.map(tag =>
+                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+              )
             )
           : null
       ),

+ 98 - 37
src/views/transfer_view.js

@@ -20,32 +20,64 @@ const generateTransferActions = (transfer, userId) => {
 
 const generateTransferCard = (transfer, userId) => {
   return div({ class: "transfer-item" },
-    generateTransferActions(transfer, userId),
-    form({ method: "GET", action: `/transfers/${encodeURIComponent(transfer.id)}` },
-      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-    ),
-    h2(`${i18n.transfersConcept}: ${transfer.concept}`),
-    p(`${i18n.transfersFrom}: `, a({ href: `/author/${encodeURIComponent(transfer.from)}`, target: "_blank" }, transfer.from)),
-    p(`${i18n.transfersTo}: `, a({ href: `/author/${encodeURIComponent(transfer.to)}`, target: "_blank" }, transfer.to)),
-    p(`${i18n.transfersAmount}: ${transfer.amount} ECO`),
-    p(`${i18n.transfersDeadline}: ${moment(transfer.deadline).format("YYYY-MM-DD HH:mm")}`),
-    p(`${i18n.transfersCreatedAt}: ${moment(transfer.createdAt).format("YYYY-MM-DD HH:mm")}`),
-    p(`${i18n.transfersStatus}: ${i18n[`transfersStatus${transfer.status.charAt(0) + transfer.status.slice(1).toLowerCase()}`]}`),
-    p(`${i18n.transfersConfirmations}: ${transfer.confirmedBy.length}/2`),
-    (transfer.status === 'UNCONFIRMED' && transfer.to === userId)
-      ? form({ method: "POST", action: `/transfers/confirm/${encodeURIComponent(transfer.id)}` },
-          button({ type: "submit" }, i18n.transfersConfirmButton), br(), br()
-        )
-      : null,
-    transfer.tags && transfer.tags.length
-      ? div(
-          transfer.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right:0.8em;margin-bottom:0.5em;" }, `#${tag}`))
-        )
-      : null,
-    div({ class: "voting-buttons" },
-      ["interesting", "necessary", "funny", "disgusting", "sensible", "propaganda", "adultOnly", "boring", "confusing", "inspiring", "spam"].map(category =>
-        form({ method: "POST", action: `/transfers/opinions/${encodeURIComponent(transfer.id)}/${category}` },
-          button({ class: "vote-btn" }, `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${transfer.opinions?.[category] || 0}]`)
+    div({ class: 'card-section transfer' },
+      generateTransferActions(transfer, userId),
+      form({ method: "GET", action: `/transfers/${encodeURIComponent(transfer.id)}` },
+        button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+      ),
+      br,
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersConcept}:`),
+        span({ class: 'card-value' }, transfer.concept)
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersDeadline}:`),
+        span({ class: 'card-value' }, moment(transfer.deadline).format("YYYY-MM-DD HH:mm"))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersStatus}:`),
+        span({ class: 'card-value' }, i18n[`transfersStatus${transfer.status.charAt(0) + transfer.status.slice(1).toLowerCase()}`])
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersAmount}:`),
+        span({ class: 'card-value' }, `${transfer.amount} ECO`)
+      ),
+      br,
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersFrom}:`),
+        span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(transfer.from)}`, target: "_blank" }, transfer.from))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersTo}:`),
+        span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(transfer.to)}`, target: "_blank" }, transfer.to))
+      ),
+      br,
+      h2({ class: 'card-field' },
+        span({ class: 'card-label' }, `${i18n.transfersConfirmations}: `),
+        span({ class: 'card-value' }, `${transfer.confirmedBy.length}/2`)
+      ),
+      (transfer.status === 'UNCONFIRMED' && transfer.to === userId)
+        ? form({ method: "POST", action: `/transfers/confirm/${encodeURIComponent(transfer.id)}` },
+            button({ type: "submit" }, i18n.transfersConfirmButton), br(), br()
+          )
+        : null,
+      transfer.tags && transfer.tags.length
+        ? div({ class: 'card-tags' },
+            transfer.tags.map(tag =>
+              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right:0.8em;margin-bottom:0.5em;" }, `#${tag}`)
+            )
+          )
+        : null, 
+      br,
+      p({ class: 'card-footer' },
+        span({ class: 'date-link' }, `${transfer.createdAt} ${i18n.performed} `),
+        a({ href: `/author/${encodeURIComponent(transfer.from)}`, class: 'user-link' }, `${transfer.from}`)
+      ), 
+      div({ class: "voting-buttons" },
+        ["interesting", "necessary", "funny", "disgusting", "sensible", "propaganda", "adultOnly", "boring", "confusing", "inspiring", "spam"].map(category =>
+          form({ method: "POST", action: `/transfers/opinions/${encodeURIComponent(transfer.id)}/${category}` },
+            button({ class: "vote-btn" }, `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${transfer.opinions?.[category] || 0}]`)
+          )
         )
       )
     )
@@ -136,26 +168,55 @@ exports.singleTransferView = async (transfer, filter) => {
           button({ type: 'submit', name: 'filter', value: 'create', class: "create-button" }, i18n.transfersCreateButton)
         )
       ),
-      div({ class: "tags-header" },
-        h2(transfer.concept),
-        p(`${i18n.transfersFrom}: `, a({ href: `/author/${encodeURIComponent(transfer.from)}`, target: "_blank" }, transfer.from)),
-        p(`${i18n.transfersTo}: `, a({ href: `/author/${encodeURIComponent(transfer.to)}`, target: "_blank" }, transfer.to)),
-        p(`${i18n.transfersAmount}: ${transfer.amount} ECO`),
-        p(`${i18n.transfersDeadline}: ${moment(transfer.deadline).format("YYYY-MM-DD HH:mm")}`),
-        p(`${i18n.transfersCreatedAt}: ${moment(transfer.createdAt).format("YYYY-MM-DD HH:mm")}`),
-        p(`${i18n.transfersStatus}: ${i18n[`transfersStatus${transfer.status.charAt(0) + transfer.status.slice(1).toLowerCase()}`]}`),
-        p(`${i18n.transfersConfirmations}: ${transfer.confirmedBy.length}/2`)
-      ),
+	div({ class: "tags-header" },
+	  div({ class: 'card-section transfer' },
+            div({ class: 'card-field' },
+             span({ class: 'card-label' }, `${i18n.transfersConcept}:`),
+             span({ class: 'card-value' }, transfer.concept)
+            ),
+            div({ class: 'card-field' },
+	      span({ class: 'card-label' }, `${i18n.transfersDeadline}:`),
+	      span({ class: 'card-value' }, moment(transfer.deadline).format("YYYY-MM-DD HH:mm"))
+	    ),
+	    div({ class: 'card-field' },
+	      span({ class: 'card-label' }, `${i18n.transfersStatus}:`),
+	      span({ class: 'card-value' }, i18n[`transfersStatus${transfer.status.charAt(0) + transfer.status.slice(1).toLowerCase()}`])
+	    ),
+	    div({ class: 'card-field' },
+	      span({ class: 'card-label' }, `${i18n.transfersAmount}:`),
+	      span({ class: 'card-value' }, `${transfer.amount} ECO`)
+	    ),
+	    br,
+	    div({ class: 'card-field' },
+	      span({ class: 'card-label' }, `${i18n.transfersFrom}:`),
+	      span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(transfer.from)}`, target: "_blank" }, transfer.from))
+	    ),
+	    div({ class: 'card-field' },
+	      span({ class: 'card-label' }, `${i18n.transfersTo}:`),
+	      span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(transfer.to)}`, target: "_blank" }, transfer.to))
+	    ),
+            br,
+	    h2({ class: 'card-field' },
+	      span({ class: 'card-label' }, `${i18n.transfersConfirmations}: `),
+	      span({ class: 'card-value' }, `${transfer.confirmedBy.length}/2`)
+	    )
+	  )
+	),
       transfer.status === 'UNCONFIRMED' && transfer.to === userId
         ? form({ method: "POST", action: `/transfers/confirm/${encodeURIComponent(transfer.id)}` },
             button({ type: "submit" }, i18n.transfersConfirmButton), br(), br()
           )
         : null,
       transfer.tags && transfer.tags.length
-        ? div(
+          ? div({ class: 'card-tags' },
             transfer.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right:0.8em;margin-bottom:0.5em;" }, `#${tag}`))
           )
         : null,
+       br,
+       p({ class: 'card-footer' },
+        span({ class: 'date-link' }, `${transfer.createdAt} ${i18n.performed} `),
+        a({ href: `/author/${encodeURIComponent(transfer.from)}`, class: 'user-link' }, `${transfer.from}`)
+      ),
       div({ class: "voting-buttons" },
         ["interesting", "necessary", "funny", "disgusting", "sensible", "propaganda", "adultOnly", "boring", "confusing", "inspiring", "spam"].map(category =>
           form({ method: "POST", action: `/transfers/opinions/${encodeURIComponent(transfer.id)}/${category}` },

+ 123 - 177
src/views/trending_view.js

@@ -1,4 +1,4 @@
-const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, img, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe");
+const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, img, video: videoHyperaxe, audio: audioHyperaxe, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const { renderTextWithStyles } = require('../backend/renderTextWithStyles');
 const { config } = require('../server/SSB_server.js');
@@ -22,219 +22,162 @@ const renderTrendingCard = (item, votes, categories) => {
 
   let contentHtml;
   
-  if (c.type === 'event') {
-    const { title, description, date, location, price, url: eventUrl, attendees, tags, organizer, status, isPublic, id } = c;
-    contentHtml = div({ class: 'trending-event' },
-    form({ method: "GET", action: `/events/${encodeURIComponent(item.key)}` },
-      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-    ),
-    h2(title),
-    description ? p(`${i18n.trendingDescription}: ${description}`) : "",
-    date ? p(`${i18n.trendingDate}: ${new Date(date).toLocaleString()}`) : "",
-    location ? p(`${i18n.trendingLocation}: ${location}`) : "",
-    status ? p(`${i18n.trendingStatus}: ${status}`) : "",
-    typeof isPublic === 'boolean' ? p(`${i18n.trendingIsPublic || 'Public'}: ${isPublic ? 'Yes' : 'No'}`) : "",
-    price ? p(`${i18n.trendingPrice}: ${price} ECO`) : "",
-    eventUrl ? p(`${i18n.trendingUrl}: `, a({ href: eventUrl, target: '_blank' }, eventUrl)) : "",
-    organizer ? p(`${i18n.trendingOrganizer || 'Organizer'}: `, a({ href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
-    Array.isArray(attendees) ? p(`${i18n.attendees}: ${attendees.length}`) : "",
-    tags?.length
-      ? div(tags.map(tag =>
-          a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-        ))
-      : null
-    );
-  } else if (c.type === 'bookmark') {
+  if (c.type === 'bookmark') {
     const { author, url, tags, description, category, lastVisit } = c;
     contentHtml = div({ class: 'trending-bookmark' },
-    form({ method: "GET", action: `/bookmarks/${encodeURIComponent(item.key)}` },
-      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-    ),
-    description ? p(`${description}`) : "",
-    h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : ""),
-    category ? p(`${i18n.bookmarkCategory}: ${category}`) : "",
-    lastVisit ? p(`${i18n.bookmarkLastVisit}: ${new Date(lastVisit).toLocaleString()}`) : "",
-    tags?.length
-      ? div(tags.map(tag =>
-          a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-        ))
-      : ""
-  );
-  } else if (c.type === 'task') {
-    const { title, description, startTime, endTime, priority, location, tags, isPublic, assignees, status, author } = c;
-    contentHtml = div({ class: 'trending-task' },
-      form({ method: "GET", action: `/tasks/${encodeURIComponent(item.key)}` },
+    div({ class: 'card-section bookmark' }, 
+      form({ method: "GET", action: `/bookmarks/${encodeURIComponent(item.key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
       ),
-      h2(title),
-      description ? p(`${i18n.trendingDescription}: ${description}`) : "",
-      startTime ? p(`${i18n.trendingStart}: ${new Date(startTime).toLocaleString()}`) : "",
-      endTime ? p(`${i18n.trendingEnd}: ${new Date(endTime).toLocaleString()}`) : "",
-      priority ? p(`${i18n.trendingPriority}: ${priority}`) : "",
-      location ? p(`${i18n.trendingLocation}: ${location}`) : "",
-      tags?.length
-        ? div(tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-          ))
-        : "",
-      typeof isPublic === 'boolean' ? p(`${i18n.trendingIsPublic || 'Public'}: ${isPublic ? 'Yes' : 'No'}`) : "",
-      Array.isArray(assignees) ? p(`${i18n.trendingAssignees || 'Assignees'}: ${assignees.length}`) : "",
-      status ? p(`${i18n.trendingStatus}: ${status}`) : "",
-      author ? p(`${i18n.trendingAuthor || 'Author'}: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)) : ""
-    );
+      br,
+      description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ':'), span({ class: 'card-value' }, description)) : "",
+      h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : ""),
+      lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(lastVisit).toLocaleString())) : ""
+    )
+  );
   } else if (c.type === 'image') {
     const { url, title, description, tags, meme } = c;
     contentHtml = div({ class: 'trending-image' },
+    div({ class: 'card-section image' },
       form({ method: "GET", action: `/images/${encodeURIComponent(item.key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
       ),
-      title ? h2(title) : "",
-      description ? p(description) : "",
-      meme ? h2(`${i18n.trendingCategory}: ${i18n.meme}`) : "",
-      img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image' }),
       br,
-      tags?.length
-        ? div(tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-          ))
-        : ""
-    );
+      title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, title)) : "",
+      description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, description)) : "",
+      meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : "",
+      br,
+      div({ class: 'card-field' }, img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image' }))
+    )
+  );
   } else if (c.type === 'audio') {
     const { url, mimeType, title, description } = c;
     contentHtml = div({ class: 'trending-audio' },
+    div({ class: 'card-section audio' },
       form({ method: "GET", action: `/audios/${encodeURIComponent(item.key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-    ),
-      title?.trim() ? h2(title) : "",
-      description?.trim() ? p(description) : "",
+      ),
+      br,
+      title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, title)) : "",
+      description?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'), span({ class: 'card-value' }, description)) : "",
+      br,
       url
-        ? div({ class: "audio-container" },
+        ? div({ class: 'card-field audio-container' },
             audioHyperaxe({
               controls: true,
               src: `/blob/${encodeURIComponent(url)}`,
               type: mimeType
             })
           )
-        : p(i18n.audioNoFile)
-    );
+        : div({ class: 'card-field' }, p(i18n.audioNoFile))
+    )
+  );
   } else if (c.type === 'video') {
     const { url, mimeType, title, description } = c;
     contentHtml = div({ class: 'trending-video' },
+    div({ class: 'card-section video' },
       form({ method: "GET", action: `/videos/${encodeURIComponent(item.key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
       ),
-      title?.trim() ? h2(title) : "",
-      description?.trim() ? p(description) : "",
+      br,
+      title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, title)) : "",
+      description?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'), span({ class: 'card-value' }, description)) : "",
+      br,
       url
-      ? div({ class: "video-container" },
-          videoHyperaxe({
-            controls: true,
-            src: `/blob/${encodeURIComponent(url)}`,
-            type: mimeType,
-            preload: 'metadata',
-            width: '640',
-            height: '360'
-          })
-        )
-      : p(i18n.videoNoFile)
+        ? div({ class: 'card-field video-container' },
+            videoHyperaxe({
+              controls: true,
+              src: `/blob/${encodeURIComponent(url)}`,
+              type: mimeType,
+              preload: 'metadata',
+              width: '640',
+              height: '360'
+            })
+          )
+        : div({ class: 'card-field' }, p(i18n.videoNoFile))
+    )
   );
   } else if (c.type === 'document') {
     const { url, title, description, tags = [], key } = c;
     contentHtml = div({ class: 'trending-document' },
-    form({ method: "GET", action: `/documents/${encodeURIComponent(item.key)}` },
-      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-    ),
-    br,
-    title?.trim() ? h2(title) : "",
-    description?.trim() ? p(description) : "",
-    div({
-      id: `pdf-container-${key || url}`,
-      class: 'pdf-viewer-container',
-      'data-pdf-url': `/blob/${encodeURIComponent(url)}`
-    }),
-    tags.length
-      ? div(tags.map(tag =>
-          a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-        ))
-      : null
-     );
-    } else if (c.type === 'feed') {
-      const { text, author, createdAt, opinions, opinions_inhabitants, refeeds, refeeds_inhabitants } = c;
-      contentHtml = div({ class: 'trending-feed' },
-        h2(text),
-        p(i18n.author + ": ", a({ href: `/author/${encodeURIComponent(author)}`, target: "_blank" }, author)),
-        p(i18n.createdAt + ": " + new Date(createdAt).toLocaleString()),
-        h2(i18n.tribeFeedRefeeds + ": " + refeeds)
-     );
-     } else if (c.type === 'votes') {
-      const { question, deadline, status, votes, totalVotes } = c;
-      const votesList = votes && typeof votes === 'object'
-        ? Object.entries(votes).map(([option, count]) => ({ option, count }))
-        : [];
-      contentHtml = div({ class: 'trending-votes' },
-        form({ method: "GET", action: `/votes/${encodeURIComponent(item.key)}` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        h2(question),
-        p(`${i18n.deadline}: ${deadline ? new Date(deadline).toLocaleString() : ''}`),
-        h2(`${i18n.voteTotalVotes}: ${totalVotes}`),
-        table(
-          tr(...votesList.map(({ option }) => th(i18n[option] || option))),
-          tr(...votesList.map(({ count }) => td(count)))
-        )
-      );
-    } else if (c.type === 'report') {
-      const { title, description, category, createdAt, author, image, tags, confirmations, severity, status, isAnonymous } = c;
-      contentHtml = div({ class: 'trending-report' },
-        form({ method: "GET", action: `/reports/${encodeURIComponent(item.id)}` },
-          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-        ),
-        p(`${i18n.title}: ${title}`),
-        description ? p(`${i18n.description}: ${description}`) : "",
-        category ? p(`${i18n.category}: ${category}`) : "",
-        severity ? p(`${i18n.severity || 'Severity'}: ${severity}`) : "",
-        status ? p(`${i18n.status}: ${status}`) : "",
-        image ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image' }) : "",
-        createdAt ? p(`${i18n.date}: ${new Date(createdAt).toLocaleString()}`) : "",
-        typeof isAnonymous === 'boolean'
-          ? p(`${i18n.author || 'Author'}: `, isAnonymous
-          ? i18n.reportsAnonymousAuthor || 'Anonymous'
-          : a({ href: `/author/${encodeURIComponent(author)}`, target: '_blank' }, author))
-          : author ? p(`${i18n.author || 'Author'}: `, a({ href: `/author/${encodeURIComponent(author)}`, target: '_blank' }, author)) : "",
-        Array.isArray(confirmations) ? h2(`${i18n.confirmations || 'Confirmations'}: ${confirmations.length}`) : "",
-        tags?.length
-          ? div(tags.map(tag =>
-          a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
-            ))
-          : ""
-      );
-    } else if (c.type === 'transfer') {
-      const { from, to, concept, amount, deadline, status, tags, confirmedBy } = c;
-      contentHtml = div({ class: 'trending-transfer' },
+    div({ class: 'card-section document' },
+      form({ method: "GET", action: `/documents/${encodeURIComponent(item.key)}` },
+        button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+      ),
+      br,
+      title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), span({ class: 'card-value' }, title)) : "",
+      description?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentDescriptionLabel + ':'), span({ class: 'card-value' }, description)) : "",
+      br,
+      div({
+        id: `pdf-container-${key || url}`,
+        class: 'card-field pdf-viewer-container',
+        'data-pdf-url': `/blob/${encodeURIComponent(url)}`
+      })
+    )
+  );
+  } else if (c.type === 'feed') {
+    const { text, refeeds } = c;
+    contentHtml = div({ class: 'trending-feed' },
+    div({ class: 'card-section feed' },
+      h2(text),
+      h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-label' }, refeeds))
+    )
+  );
+  } else if (c.type === 'votes') {
+    const { question, deadline, status, votes, totalVotes } = c;
+    const votesList = votes && typeof votes === 'object'
+    ? Object.entries(votes).map(([option, count]) => ({ option, count }))
+    : [];
+    contentHtml = div({ class: 'trending-votes' },
+    div({ class: 'card-section votes' },
+      form({ method: "GET", action: `/votes/${encodeURIComponent(item.key)}` },
+        button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+      ),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteQuestionLabel + ':'), span({ class: 'card-value' }, question)),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), span({ class: 'card-value' }, totalVotes)),
+      table(
+        tr(...votesList.map(({ option }) => th(i18n[option] || option))),
+        tr(...votesList.map(({ count }) => td(count)))
+      )
+    )
+  );
+  } else if (c.type === 'transfer') {
+    const { from, to, concept, amount, deadline, status, tags, confirmedBy } = c;
+    contentHtml = div({ class: 'trending-transfer' },
+    div({ class: 'card-section transfer' },
       form({ method: "GET", action: `/transfers/${encodeURIComponent(item.key)}` },
         button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
       ),
-      h2(i18n.concept + ": " + concept),
-      p(i18n.from + ": ", a({ href: `/author/${encodeURIComponent(from)}`, target: "_blank" }, from)),
-      p(i18n.to + ": ", a({ href: `/author/${encodeURIComponent(to)}`, target: "_blank" }, to)),
-      h2(i18n.amount + ": " + amount),
-      p(i18n.deadline + ": " + (deadline ? new Date(deadline).toLocaleString() : "")),
-      p(i18n.status + ": " + status),
-      p(`${i18n.transfersConfirmations}: ${confirmedBy.length}/2`),
-      tags?.length
-        ? div(tags.map(tag =>
-            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-          ))
-        : ""
-    );
+      br,
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)),
+      br,
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(from)}`, target: "_blank" }, from))),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(to)}`, target: "_blank" }, to))),
+      br,
+      div({ class: 'card-field' }, h2({ class: 'card-label' }, i18n.transfersConfirmations + ': ' + `${confirmedBy.length}/2`))
+    )
+  );
   } else {
-    contentHtml = div({ class: 'styled-text', innerHTML: renderTextWithStyles(c.text || c.description || c.title || '[no content]') });
+    contentHtml = div({ class: 'styled-text' },
+    div({ class: 'card-section styled-text-content' },
+    div({ class: 'card-field' }, 
+      span({ class: 'card-label' }, i18n.textContentLabel + ':'), 
+      span({ class: 'card-value', innerHTML: renderTextWithStyles(c.text || c.description || c.title || '[no content]') })
+     )
+    )
+   );
   }
 
   return div({ class: 'trending-card', style: 'background-color:#2c2f33;border-radius:8px;padding:16px;border:1px solid #444;' },
     contentHtml,
-    p(`${i18n.trendingAuthor}: `, a({ href: `/author/${encodeURIComponent(item.value.author)}` }, item.value.author)),
-    p(`${i18n.trendingCreatedAtLabel || i18n.trendingCreatedAt}: ${created}`),
+    p({ class: 'card-footer' },
+      span({ class: 'date-link' }, `${created} ${i18n.performed} `),
+      a({ href: `/author/${encodeURIComponent(item.value.author)}`, class: 'user-link' }, `${item.value.author}`)
+    ),  
     h2(`${i18n.trendingTotalOpinions || i18n.trendingTotalCount}: ${votes}`),
     div({ class: "voting-buttons" },
       categories.map(cat =>
@@ -250,12 +193,15 @@ exports.trendingView = (items, filter, categories) => {
   const title = i18n.trendingTitle;
   const baseFilters = ['RECENT', 'ALL', 'MINE', 'TOP'];
   const contentFilters = [
-    ['bookmark', 'event', 'task'],
-    ['votes', 'report', 'feed'],
-    ['image', 'video', 'audio', 'document'],
-    ['transfer']
+    ['votes', 'feed', 'transfer'],
+    ['bookmark', 'image', 'video', 'audio', 'document']
   ];
-  let filteredItems = items.filter(item => item.value.content.type !== 'tombstone');
+  let filteredItems = items.filter(item => {
+    const content = item.value?.content || item.content;
+    if (!content || typeof content !== 'object') return false;
+    if (content.type === 'tombstone') return false;
+    return true;
+  });
   if (filter === 'ALL') {
   } else if (filter === 'MINE') {
     filteredItems = filteredItems.filter(item => item.value.author === userId);

+ 16 - 13
src/views/tribes_view.js

@@ -73,7 +73,7 @@ const renderFeedTribesView = (tribe, page, query, filter) => {
                   : p(i18n.alreadyRefeeded)
               ),
               div({ class: 'feed-main' },
-                p(`${new Date(m.date).toLocaleString()} — `, a({ href: `/author/${encodeURIComponent(m.author)}` }, m.author)),
+                p(`${new Date(m.date).toLocaleString()} — `, a({ class: 'user-link', href: `/author/${encodeURIComponent(m.author)}` }, m.author)),
                 p(m.message)
               )
             )
@@ -263,18 +263,18 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}) => {
         ),
         h2(t.title)
       ),
-      img({ src: imageSrc }),
-      p(t.description),
-      p(`${i18n.tribeLocationLabel}: ${t.location}`),
       p(`${i18n.tribeIsAnonymousLabel}: ${t.isAnonymous ? i18n.tribePrivate : i18n.tribePublic}`),
       p(`${i18n.tribeModeLabel}: ${t.inviteMode.toUpperCase()}`),
       p(`${i18n.tribeLARPLabel}: ${t.isLARP ? i18n.tribeYes : i18n.tribeNo}`),
-      p(`${i18n.tribeMembersCount}: ${t.members.length}`),
+      img({ src: imageSrc }),
+      p(t.description),
+      p(`${i18n.tribeLocationLabel}: ${t.location}`),
+      h2(`${i18n.tribeMembersCount}: ${t.members.length}`),
       t.tags && t.tags.filter(Boolean).length ? div(t.tags.filter(Boolean).map(tag =>
         a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link', style: 'margin-right:0.8em;margin-bottom:0.5em;' }, `#${tag}`)
-      )) : null,
+      )) : null,    
       p(`${i18n.tribeCreatedAt}: ${new Date(t.createdAt).toLocaleString()}`),
-      p(`${i18n.tribeAuthor}: `, a({ href: `/author/${encodeURIComponent(t.author)}` }, t.author)),
+      p(a({ class: 'user-link', href: `/author/${encodeURIComponent(t.author)}` }, t.author)),
       t.members.includes(userId) ? div(
       form({ method: 'POST', action: '/tribes/generate-invite' }, 
         input({ type: 'hidden', name: 'tribeId', value: t.id }),
@@ -355,7 +355,7 @@ const renderFeedTribeView = async (tribe, query = {}, filter) => {
                   : p(i18n.alreadyRefeeded)
               ),
               div({ class: 'feed-main' },
-                p(`${new Date(m.date).toLocaleString()} — `, a({ href: `/author/${encodeURIComponent(m.author)}` }, m.author)),
+                p(`${new Date(m.date).toLocaleString()} — `, a({ class: 'user-link', href: `/author/${encodeURIComponent(m.author)}` }, m.author)),
                 p(m.message)
               )
             )
@@ -375,15 +375,18 @@ exports.tribeView = async (tribe, userId, query) => {
   const pageTitle = tribe.title;
   const tribeDetails = div({ class: 'tribe-details' },
     h2(tribe.title),
-    img({ src: imageSrc, alt: tribe.title }),
-    p(tribe.description),
-    p(`${i18n.tribeLocationLabel}: ${tribe.location}`),
     p(`${i18n.tribeIsAnonymousLabel}: ${tribe.isAnonymous ? i18n.tribePrivate : i18n.tribePublic}`),
     p(`${i18n.tribeModeLabel}: ${tribe.inviteMode.toUpperCase()}`),
     p(`${i18n.tribeLARPLabel}: ${tribe.isLARP ? i18n.tribeYes : i18n.tribeNo}`),
-    p(`${i18n.tribeMembersCount}: ${tribe.members.length}`),
+    img({ src: imageSrc, alt: tribe.title }),
+    p(tribe.description),
+    p(`${i18n.tribeLocationLabel}: ${tribe.location}`),
+    h2(`${i18n.tribeMembersCount}: ${tribe.members.length}`),
+    tribe.tags && tribe.tags.filter(Boolean).length ? div(tribe.tags.filter(Boolean).map(tag =>
+      a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link', style: 'margin-right:0.8em;margin-bottom:0.5em;' }, `#${tag}`)
+    )) : null,  
     p(`${i18n.tribeCreatedAt}: ${new Date(tribe.createdAt).toLocaleString()}`),
-    p(`${i18n.tribeAuthor}: `, a({ href: `/author/${encodeURIComponent(tribe.author)}` }, tribe.author)),
+    p(a({ class: 'user-link', href: `/author/${encodeURIComponent(tribe.author)}` }, tribe.author)),
     div({ class: 'tribe-feed-form' }, tribe.members.includes(config.keys.id)
       ? form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/message` },
           textarea({ name: 'message', rows: 3, cols: 50, maxlength: 280, placeholder: i18n.tribeFeedMessagePlaceholder }),

+ 39 - 29
src/views/video_view.js

@@ -1,9 +1,9 @@
-const { form, button, div, h2, p, section, input, label, br, a, video: videoHyperaxe } = require("../server/node_modules/hyperaxe");
+const { form, button, div, h2, p, section, input, label, br, a, video: videoHyperaxe, span } = require("../server/node_modules/hyperaxe");
 const moment = require("../server/node_modules/moment");
 const { template, i18n } = require('./main_views');
 const { config } = require('../server/SSB_server.js');
 
-const userId = config.keys.id
+const userId = config.keys.id;
 
 const getFilteredVideos = (filter, videos, userId) => {
   const now = Date.now();
@@ -34,11 +34,11 @@ const renderVideoActions = (filter, video) => {
 const renderVideoList = (filteredVideos, filter) => {
   return filteredVideos.length > 0
     ? filteredVideos.map(video =>
-        div({ class: "video-item" },
+        div({ class: "tags-header" },
           renderVideoActions(filter, video),
           form({ method: "GET", action: `/videos/${encodeURIComponent(video.key)}` },
-	  button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)),
-	  br,
+            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)),
+          video.title?.trim() ? h2(video.title) : null,
           video.url
             ? div({ class: "video-container" },
                 videoHyperaxe({
@@ -50,16 +50,20 @@ const renderVideoList = (filteredVideos, filter) => {
                   height: '360'
                 })
               )
-            : p(i18n.videoNoFile),
-          p(`${i18n.videoCreatedAt}: ${moment(video.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-          p(`${i18n.videoAuthor}: `, a({ href: `/author/${encodeURIComponent(video.author)}` }, video.author)),
-          video.title?.trim() ? h2(video.title) : null,
+            : p(i18n.videoNoFile),        
           video.description?.trim() ? p(video.description) : null,
           video.tags?.length
-            ? div(video.tags.map(tag =>
-                a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-              ))
+            ? div({ class: "card-tags" },
+                video.tags.map(tag =>
+                  a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+                )
+              )
             : null,
+          br,
+          p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(video.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(video.author)}`, class: 'user-link' }, `${video.author}`)
+          ),
           div({ class: "voting-buttons" },
             ['interesting','necessary','funny','disgusting','sensible',
              'propaganda','adultOnly','boring','confusing','inspiring','spam']
@@ -154,8 +158,17 @@ exports.singleVideoView = async (video, filter) => {
         )
       ),
       div({ class: "tags-header" },
+        isAuthor ? div({ class: "video-actions" },
+        !hasOpinions
+          ? form({ method: "GET", action: `/videos/edit/${encodeURIComponent(video.key)}` },
+              button({ class: "update-btn", type: "submit" }, i18n.videoUpdateButton)
+            )
+          : null,
+        form({ method: "POST", action: `/videos/delete/${encodeURIComponent(video.key)}` },
+          button({ class: "delete-btn", type: "submit" }, i18n.videoDeleteButton)
+        )
+      ) : null,
         h2(video.title),
-        p(video.description),
         video.url
           ? div({ class: "video-container" },
               videoHyperaxe({
@@ -168,24 +181,20 @@ exports.singleVideoView = async (video, filter) => {
               })
             )
           : p(i18n.videoNoFile),
-        p(`${i18n.videoCreatedAt}: ${moment(video.createdAt).format('YYYY/MM/DD HH:mm:ss')}`),
-        p(`${i18n.videoAuthor}: `, a({ href: `/author/${encodeURIComponent(video.author)}` }, video.author)),
+        p(video.description),
         video.tags?.length
-          ? div(video.tags.map(tag =>
-              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
-            ))
-          : null
+            ? div({ class: "card-tags" },
+                video.tags.map(tag =>
+                  a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link", style: "margin-right: 0.8em; margin-bottom: 0.5em;" }, `#${tag}`)
+                )
+              )
+            : null,
+          br,
+          p({ class: 'card-footer' },
+          span({ class: 'date-link' }, `${moment(video.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+          a({ href: `/author/${encodeURIComponent(video.author)}`, class: 'user-link' }, `${video.author}`)
+          ),
       ),
-      isAuthor ? div({ class: "video-actions" },
-        !hasOpinions
-          ? form({ method: "GET", action: `/videos/edit/${encodeURIComponent(video.key)}` },
-              button({ class: "update-btn", type: "submit" }, i18n.videoUpdateButton)
-            )
-          : null,
-        form({ method: "POST", action: `/videos/delete/${encodeURIComponent(video.key)}` },
-          button({ class: "delete-btn", type: "submit" }, i18n.videoDeleteButton)
-        )
-      ) : null,
       div({ class: "voting-buttons" },
         ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
           form({ method: "POST", action: `/videos/opinions/${encodeURIComponent(video.key)}/${category}` },
@@ -196,3 +205,4 @@ exports.singleVideoView = async (video, filter) => {
     )
   );
 };
+

+ 39 - 27
src/views/vote_view.js

@@ -1,22 +1,19 @@
-const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label } = require("../server/node_modules/hyperaxe");
+const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 const moment = require('../server/node_modules/moment');
 const { config } = require('../server/SSB_server.js');
 
 const userId = config.keys.id;
 
-const generateFilterButtons = (filters, currentFilter, action) => {
-  return filters.map(mode =>
-    form({ method: 'GET', action },
-      input({ type: 'hidden', name: 'filter', value: mode }),
-      button({ type: 'submit', class: currentFilter === mode.toLowerCase() ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
-    )
-  );
-};
-
 const voteLabel = opt =>
   i18n['vote' + opt.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join('')] || opt;
 
+const renderStyledField = (labelText, valueElement) =>
+  div({ class: 'card-field' },
+    span({ class: 'card-label' }, labelText),
+    span({ class: 'card-value' }, valueElement)
+  );
+
 const renderVoteCard = (v, voteOptions, firstRow, secondRow, userId, filter) => {
   const baseCounts = voteOptions.reduce((acc, opt) => { acc[opt] = v.votes?.[opt] || 0; return acc }, {});
   const maxOpt = voteOptions.filter(opt => opt !== 'FOLLOW_MAJORITY')
@@ -30,7 +27,7 @@ const renderVoteCard = (v, voteOptions, firstRow, secondRow, userId, filter) =>
   const showUpdateButton = filter === 'mine' && !Object.values(v.opinions || {}).length;
   const showDeleteButton = filter === 'mine';
 
-  return div({ class: 'vote-item' },
+  return div({ class: 'card card-section vote' },
     filter === 'mine' ? div({ class: 'vote-actions' },
       showUpdateButton
         ? form({ method: 'GET', action: `/votes/edit/${encodeURIComponent(v.id)}` },
@@ -46,10 +43,11 @@ const renderVoteCard = (v, voteOptions, firstRow, secondRow, userId, filter) =>
     form({ method: 'GET', action: `/votes/${encodeURIComponent(v.id)}` },
       button({ class: 'filter-btn', type: 'submit' }, i18n.viewDetails)
     ),
-    h2(v.question),
-    p(`${i18n.voteDeadline}: ${moment(v.deadline).format('YYYY/MM/DD HH:mm:ss')}`),
-    p(`${i18n.voteStatus}: ${v.status}`),
-
+    br,
+    renderStyledField(i18n.voteQuestionLabel + ':', v.question),
+    renderStyledField(i18n.voteDeadline + ':', moment(v.deadline).format('YYYY/MM/DD HH:mm:ss')),
+    renderStyledField(i18n.voteStatus + ':', v.status),
+    br,
     v.status === 'OPEN'
       ? div({ class: 'vote-buttons-block' },
           div({ class: 'vote-buttons-row' },
@@ -64,21 +62,35 @@ const renderVoteCard = (v, voteOptions, firstRow, secondRow, userId, filter) =>
           )
         )
       : null,
-
-    h2(`${i18n.voteTotalVotes}: ${v.totalVotes}`),
-    table(
-      tr(...voteOptions.map(opt => th(voteLabel(opt)))),
-      tr(...voteOptions.map(opt => td(baseCounts[opt])))
+    renderStyledField(i18n.voteTotalVotes + ':', v.totalVotes),
+    br,
+    div({ class: 'vote-table' },
+      table(
+        tr(...voteOptions.map(opt => th(voteLabel(opt)))),
+        tr(...voteOptions.map(opt => td(baseCounts[opt])))
+      )
+    ),
+    renderStyledField(
+      i18n.voteBreakdown + ':',
+      span({}, [
+        voteLabel(result), ' = ', baseCounts[result],
+        ' + ', voteLabel('FOLLOW_MAJORITY'), ': ', baseCounts.FOLLOW_MAJORITY
+      ])
     ),
-    p(`${i18n.voteBreakdown}: ${voteLabel(result)} = ${baseCounts[result]} + ${voteLabel('FOLLOW_MAJORITY')}: ${baseCounts.FOLLOW_MAJORITY}`),
+    br,
     div({ class: 'vote-buttons-row' }, h2(voteLabel(result))),
-
     v.tags && v.tags.filter(Boolean).length
-      ? div(
-          v.tags.filter(Boolean).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link', style: 'margin-right:0.8em;margin-bottom:0.5em;' }, `#${tag}`))
+      ? div({ class: 'card-tags' },
+          v.tags.filter(Boolean).map(tag =>
+            a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)
+          )
         )
-      : null,
-
+      : null,    
+    br,
+    p({ class: 'card-footer' },
+      span({ class: 'date-link' }, `${moment(v.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
+      a({ href: `/author/${encodeURIComponent(v.createdBy)}`, class: 'user-link' }, `${v.createdBy}`)
+    ),
     div({ class: 'voting-buttons' },
       ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam'].map(category =>
         form({ method: 'POST', action: `/votes/opinions/${encodeURIComponent(v.id)}/${category}` },
@@ -136,7 +148,7 @@ exports.voteView = async (votes, filter, voteId) => {
       (filter === 'edit' || filter === 'create')
         ? div({ class: 'vote-form' },
             form({ action: filter === 'edit' ? `/votes/update/${encodeURIComponent(voteId)}` : '/votes/create', method: 'POST' },
-              label(i18n.voteQuestionLabel), br(),
+              h2(i18n.voteQuestionLabel),
               input({ type: 'text', name: 'question', id: 'question', required: true, value: voteToEdit.question || '' }), br(), br(),
               label(i18n.voteDeadlineLabel), br(),
               input({ type: 'datetime-local', name: 'deadline', id: 'deadline', required: true,