upstream: integrar 0.9.5 en 34 ficheros mas y portar los modulos nuevos
Segunda tanda. Resueltas por merge de tres vias 15 divergencias que no solapaban, y aplicada la version de 0.9.5 en 19 vistas y modelos donde la rama solo arrastraba codigo antiguo (firmas viejas de renderProductForm, renderJobForm, listTags sin search, y el import sin renderSpreadEditWarning). Se preserva la eliminacion de housing donde la rama lo habia descartado: modules_view, blockchain_view, search_model y tags_model. Modulos nuevos del dev portados: blog, data, gallery, mentions, polls, workflows, comments, recurrence y media_gallery, mas contentPdf, pdfDocument y content_favorites. main_views recupera renderEngagement, spreadsFor y renderSpreadEditWarning, que la rama habia eliminado y que las vistas de 0.9.5 necesitan. Sin ellas polls_view lanzaba TypeError al editar. El overlay de i18n pasa de 15 a 41 claves (46 en castellano): incorpora las 9 que upstream tenia en 0.9.1 y elimino en 0.9.5 mientras nuestro codigo las sigue usando (menuBlogs, spreadHint, chatShareUrl, forumFilterHot, publishBlog...) y las 17 de Karvan, que nunca existieron y tiraban de texto de reserva en ingles; ahora estan traducidas al castellano. Verificado con la app arrancada: declara 0.9.5, 0 claves i18n sin resolver (antes 26), 26 rutas responden 200/302, y la interfaz de la rama sale intacta: 10 categorias y 47 modulos en los hexagonos, topbar con Personal/Community y avatar, 7 accesos rapidos en la barra inferior y 0 etiquetas vacias (antes 1, la de Blogs).
This commit is contained in:
parent
f964e61a1c
commit
82b6c3ae05
43 changed files with 3221 additions and 2416 deletions
|
|
@ -89,7 +89,6 @@ code {
|
|||
}
|
||||
|
||||
.search-input,
|
||||
.feed-search-input,
|
||||
.activity-search-input {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
|
|
@ -914,3 +913,32 @@ a.create-button {
|
|||
.peers-technical-block .block-info-table td[data-label=""]::before { content: none !important; }
|
||||
.peers-list .block-info-table td .peer-key,
|
||||
.peers-technical-block .block-info-table td .peer-key { text-align: right !important; }
|
||||
|
||||
.relationship-status .relationship-actions { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; }
|
||||
.relationship-status .relationship-actions form,
|
||||
.relationship-status .relationship-actions button { width: 100% !important; }
|
||||
|
||||
.profile-sensors-box { align-items: center !important; text-align: center !important; }
|
||||
.profile-sensors-box > * { margin-left: auto !important; margin-right: auto !important; justify-content: center !important; }
|
||||
|
||||
.tribe-feed .refeed-column,
|
||||
.tribe-feed-full .refeed-column { align-self: center !important; align-items: center !important; justify-content: center !important; }
|
||||
.tribe-feed .card-footer,
|
||||
.tribe-feed-full .card-footer { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 2px !important; }
|
||||
|
||||
.invite-qr-card { display: flex !important; justify-content: center !important; border: none !important; background: transparent !important; padding: 0 !important; box-shadow: none !important; margin: 0 auto !important; }
|
||||
.invite-qr-img { display: block; margin: 0 auto; }
|
||||
|
||||
.stats-kpi { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 2px !important; }
|
||||
.stats-kpi-label,
|
||||
.stats-kpi-value { width: 100% !important; text-align: center !important; }
|
||||
|
||||
.main-column audio,
|
||||
.main-column video { width: 100% !important; max-width: 100% !important; }
|
||||
|
||||
.main-column section:has(.chat-messages-list) { border: none !important; background: transparent !important; padding: 0 !important; box-shadow: none !important; }
|
||||
.tribe-details:has(.chat-messages-list) { display: block !important; border: none !important; background: transparent !important; padding: 6px !important; margin: 0 !important; box-shadow: none !important; }
|
||||
.chat-full-width { border: none !important; background: transparent !important; padding: 0 !important; }
|
||||
.chat-messages-list { padding: 0 !important; }
|
||||
.chat-message { max-width: 92% !important; }
|
||||
.chat-message.chat-poll { max-width: 97% !important; }
|
||||
|
|
|
|||
|
|
@ -2,202 +2,490 @@
|
|||
// Se fusionan sobre las de upstream desde i18n.js, para no editar los 11
|
||||
// ficheros oasis_*.js (upstream los reescribe entero en cada release).
|
||||
//
|
||||
// Contiene: las claves que la rama anade (barra inferior bb*, menu
|
||||
// Personal/Community, karvanTitle) y los textos de upstream que la rama
|
||||
// corrige (el grupo "spread" en castellano).
|
||||
// Contiene tres grupos:
|
||||
// 1. Claves que anade la rama: barra inferior (bb*), menu Personal/Community
|
||||
// y el modulo Karvan.
|
||||
// 2. Claves que upstream tenia en 0.9.1 y elimino en 0.9.5, pero que el
|
||||
// codigo de la rama sigue usando (menuBlogs, spreadHint, chatShareUrl...).
|
||||
// 3. Textos de upstream que la rama corrige (el grupo spread en castellano).
|
||||
|
||||
module.exports = {
|
||||
en: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Category",
|
||||
bookmarkCategoryPlaceholder: "Optional",
|
||||
chatShareUrl: "Share URL",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Hot",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Blogs",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Device",
|
||||
publishBlog: "Publish Blog",
|
||||
spreadHint: "Spread this to your supporters (replicates via your feed).",
|
||||
spreadMore: "more"
|
||||
},
|
||||
es: {
|
||||
peerLastChange: "Último cambio",
|
||||
menuCommunity: "Comunidad",
|
||||
menuAll: "Todo",
|
||||
filter: "Filtrar",
|
||||
bbAdd: "Añadir",
|
||||
bbManage: "Editar",
|
||||
bbRemove: "Quitar",
|
||||
bbQuickActions: "Accesos rápidos",
|
||||
bbYourBar: "Tu barra",
|
||||
bbFull: "Máximo alcanzado",
|
||||
bbDone: "Hecho",
|
||||
bbPickTitle: "Personaliza la barra",
|
||||
bbFull: "Máximo alcanzado",
|
||||
bbManage: "Editar",
|
||||
bbPickDesc: "Elige hasta 4 accesos para la barra inferior.",
|
||||
bbPickNone: "Ninguno",
|
||||
bbPickTitle: "Personaliza la barra",
|
||||
bbQuickActions: "Accesos rápidos",
|
||||
bbRemove: "Quitar",
|
||||
bbYourBar: "Tu barra",
|
||||
bookmarkCategoryLabel: "Categoría",
|
||||
bookmarkCategoryPlaceholder: "Opcional",
|
||||
chatShareUrl: "Compartir URL",
|
||||
filter: "Filtrar",
|
||||
forumFilterHot: "Populares",
|
||||
karvanActive: "Salas activas",
|
||||
karvanBlurb: "Salas que solo viven en memoria y se autodestruyen.",
|
||||
karvanCall: "Llamar",
|
||||
karvanCreate: "Crear sala efímera",
|
||||
karvanEphemeral: "Salas efímeras",
|
||||
karvanInvite: "Invitar",
|
||||
karvanInviteBad: "✗ No es un @feed-id válido.",
|
||||
karvanInviteFail: "✗ No se pudo enviar la invitación.",
|
||||
karvanInvitePlaceholder: "Invita a un contacto por @feed-id…",
|
||||
karvanInviteSent: "✓ Invitación enviada a su buzón.",
|
||||
karvanNone: "No hay salas activas. Crea una: desaparece sola.",
|
||||
karvanRelay: "relay del servidor",
|
||||
karvanRoomName: "Nombre de la sala (opcional)",
|
||||
karvanRooms: "Salas",
|
||||
karvanSay: "Mensaje temporal…",
|
||||
karvanSend: "Enviar",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Se autodestruye en:",
|
||||
menuAll: "Todo",
|
||||
menuBlogs: "Blogs",
|
||||
menuCommunity: "Comunidad",
|
||||
peerLastChange: "Último cambio",
|
||||
profileVisibilityDevice: "Dispositivo",
|
||||
publishBlog: "Publicar Blog",
|
||||
spreadChron: "Difusión",
|
||||
spreadHint: "Difunde esto a quienes te apoyan (se replica vía tu feed).",
|
||||
spreadLabel: "Difundir",
|
||||
spreadMore: "más",
|
||||
spreaded: "Difundido",
|
||||
spreadedDescription: "Lista de posts replicados por habitante.",
|
||||
totalspreads: "Replicas totales",
|
||||
spreadLabel: "Difundir",
|
||||
spreadHint: "Difunde esto a quienes te apoyan (se replica vía tu feed)."
|
||||
totalspreads: "Replicas totales"
|
||||
},
|
||||
fr: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Catégorie",
|
||||
bookmarkCategoryPlaceholder: "Optionnel",
|
||||
chatShareUrl: "Partager URL",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Populaires",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Blogs",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Appareil",
|
||||
publishBlog: "Publier un blog",
|
||||
spreadHint: "Diffusez ceci à ceux qui vous soutiennent (réplique via votre feed).",
|
||||
spreadMore: "plus"
|
||||
},
|
||||
eu: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Kategoria",
|
||||
bookmarkCategoryPlaceholder: "Aukerakoa",
|
||||
chatShareUrl: "URLa Partekatu",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Beroa",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Blogak",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Gailua",
|
||||
publishBlog: "Bloga argitaratu",
|
||||
spreadHint: "Hedatu zure babesleei (zure feed bidez errepikatzen da).",
|
||||
spreadMore: "gehiago"
|
||||
},
|
||||
de: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Kategorie",
|
||||
bookmarkCategoryPlaceholder: "Optional",
|
||||
chatShareUrl: "URL Teilen",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Beliebt",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Blogs",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Gerät",
|
||||
publishBlog: "Blog veröffentlichen",
|
||||
spreadHint: "Verbreite dies an deine Unterstützer (über deinen Feed).",
|
||||
spreadMore: "mehr"
|
||||
},
|
||||
it: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Categoria",
|
||||
bookmarkCategoryPlaceholder: "Opzionale",
|
||||
chatShareUrl: "Condividi URL",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Caldi",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Blog",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Dispositivo",
|
||||
publishBlog: "Pubblica post",
|
||||
spreadHint: "Diffondi questo a chi ti supporta (replica via il tuo feed).",
|
||||
spreadMore: "altro"
|
||||
},
|
||||
pt: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Categoria",
|
||||
bookmarkCategoryPlaceholder: "Opcional",
|
||||
chatShareUrl: "Partilhar URL",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Populares",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Blogues",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Dispositivo",
|
||||
publishBlog: "Publicar blog",
|
||||
spreadHint: "Difunde isto aos teus apoiantes (replica pelo teu feed).",
|
||||
spreadMore: "mais"
|
||||
},
|
||||
zh: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "类别",
|
||||
bookmarkCategoryPlaceholder: "可选",
|
||||
chatShareUrl: "分享链接",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "热门",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "博客",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "设备",
|
||||
publishBlog: "发布博客",
|
||||
spreadHint: "转发给你的支持者(通过你的 feed 复制)。",
|
||||
spreadMore: "更多"
|
||||
},
|
||||
ar: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "الفئة",
|
||||
bookmarkCategoryPlaceholder: "اختياري",
|
||||
chatShareUrl: "مشاركة الرابط",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "رائج",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "مدونات",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "الجهاز",
|
||||
publishBlog: "نشر مدونة",
|
||||
spreadHint: "نشر هذا لداعميك (يتم النسخ عبر تغذيتك).",
|
||||
spreadMore: "المزيد"
|
||||
},
|
||||
hi: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "श्रेणी",
|
||||
bookmarkCategoryPlaceholder: "वैकल्पिक",
|
||||
chatShareUrl: "URL शेयर करें",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "लोकप्रिय",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "ब्लॉग",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "डिवाइस",
|
||||
publishBlog: "ब्लॉग प्रकाशित करें",
|
||||
spreadHint: "अपने समर्थकों को फैलाएँ (फ़ीड से प्रतिकृति)।",
|
||||
spreadMore: "और"
|
||||
},
|
||||
ru: {
|
||||
peerLastChange: "Last change",
|
||||
menuCommunity: "Community",
|
||||
menuAll: "All",
|
||||
filter: "Filter",
|
||||
bbAdd: "Add",
|
||||
bbManage: "Edit",
|
||||
bbRemove: "Remove",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbYourBar: "Your bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbDone: "Done",
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbFull: "Maximum reached",
|
||||
bbManage: "Edit",
|
||||
bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.",
|
||||
bbPickNone: "None",
|
||||
karvanTitle: "Karvan"
|
||||
bbPickTitle: "Customize the bar",
|
||||
bbQuickActions: "Quick actions",
|
||||
bbRemove: "Remove",
|
||||
bbYourBar: "Your bar",
|
||||
bookmarkCategoryLabel: "Категория",
|
||||
bookmarkCategoryPlaceholder: "Необязательно",
|
||||
chatShareUrl: "Поделиться URL",
|
||||
filter: "Filter",
|
||||
forumFilterHot: "Популярные",
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
menuAll: "All",
|
||||
menuBlogs: "Блоги",
|
||||
menuCommunity: "Community",
|
||||
peerLastChange: "Last change",
|
||||
profileVisibilityDevice: "Устройство",
|
||||
publishBlog: "Опубликовать блог",
|
||||
spreadHint: "Распространить вашим сторонникам (через ваш feed).",
|
||||
spreadMore: "ещё"
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const collectLocalIPs = () => {
|
|||
|
||||
module.exports = ({ host, port, middleware, allowHost }) => {
|
||||
const assets = new Koa()
|
||||
assets.use(koaStatic(join(__dirname, "..", "client", "assets")));
|
||||
assets.use(koaStatic(join(__dirname, "..", "client", "assets"), { maxage: 60 * 60 * 1000 }));
|
||||
|
||||
const app = new Koa();
|
||||
const validHosts = [];
|
||||
|
|
@ -79,12 +79,14 @@ module.exports = ({ host, port, middleware, allowHost }) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const httpDebug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true';
|
||||
|
||||
app.on("error", (err, ctx) => {
|
||||
if (err && (err.code === 'ECONNRESET' || err.code === 'EPIPE')) {
|
||||
return;
|
||||
}
|
||||
if (err && (err.name === 'BadRequestError' || err.status === 400)) {
|
||||
console.error(`[400] ${err.message}`);
|
||||
if (httpDebug) console.error(`[400] ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
console.error(err);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,8 @@
|
|||
"current": "blocks"
|
||||
},
|
||||
"modules": {
|
||||
"popularMod": "on",
|
||||
"topicsMod": "on",
|
||||
"summariesMod": "on",
|
||||
"latestMod": "on",
|
||||
"threadsMod": "on",
|
||||
"multiverseMod": "on",
|
||||
"blogsMod": "on",
|
||||
"pollsMod": "on",
|
||||
"fediverseMod": "on",
|
||||
"invitesMod": "on",
|
||||
"walletMod": "off",
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
// ties correctly — the mock and real SSB can stamp equal timestamps.
|
||||
if (prev && !((isTip && !prev.isTip) || (isTip === prev.isTip && (a.ts || 0) >= prev.ts))) continue;
|
||||
const cc = a.content || {};
|
||||
stateByRoot.set(root, { ts: a.ts || 0, isTip, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || '') });
|
||||
stateByRoot.set(root, { ts: a.ts || 0, isTip, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 });
|
||||
}
|
||||
const msgsByRoot = new Map();
|
||||
for (const a of idToAction.values()) {
|
||||
|
|
@ -181,7 +181,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
const val = await getMsg(ssbClient, root);
|
||||
const cc = val && val.content;
|
||||
if (cc && typeof cc === 'object' && cc.type === 'chat') {
|
||||
stateByRoot.set(root, { ts: 0, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || '') });
|
||||
stateByRoot.set(root, { ts: 0, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 });
|
||||
} else {
|
||||
stateByRoot.set(root, null);
|
||||
}
|
||||
|
|
@ -202,6 +202,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
chatRoot: root,
|
||||
title: info.title || '',
|
||||
description: info.description || '',
|
||||
members: info.members || 0,
|
||||
messageCount: asc.length,
|
||||
replies: asc.slice(-CHAT_THREAD_LIMIT).map(m => ({ id: m.id, author: m.author, ts: m.ts || 0, text: (m.content && m.content.text) || '' }))
|
||||
}
|
||||
});
|
||||
|
|
@ -251,6 +253,21 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
const idToAction = new Map();
|
||||
const rawById = new Map();
|
||||
const voteTally = buildVoteTally(results);
|
||||
const pollTally = new Map();
|
||||
for (const m of results) {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!c || c.type !== 'pollVote' || typeof c.target !== 'string') continue;
|
||||
if (!Array.isArray(c.choices)) continue;
|
||||
const entry = pollTally.get(c.target) || { counts: {}, voters: new Map() };
|
||||
entry.voters.set(m.value.author, { ts: m.value.timestamp || 0, choices: c.choices });
|
||||
pollTally.set(c.target, entry);
|
||||
}
|
||||
for (const entry of pollTally.values()) {
|
||||
entry.counts = {};
|
||||
for (const v of entry.voters.values()) {
|
||||
for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1;
|
||||
}
|
||||
}
|
||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||
|
||||
|
|
@ -577,6 +594,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
const actionRoot = rootOf(a.id);
|
||||
const extra = a.type === 'aiExchange' ? { helpfulVotes: aiHelpfulCounts.get(a.id) || 0 } : {};
|
||||
const voteAgg = a.type === 'votes' ? voteTally.get(a.id) : null;
|
||||
if (a.type === 'poll') {
|
||||
const tally = pollTally.get(a.id) || pollTally.get(actionRoot);
|
||||
a.pollCounts = tally ? tally.counts : {};
|
||||
a.pollVoters = tally ? tally.voters.size : 0;
|
||||
}
|
||||
let content = voteAgg ? { ...c, ...voteAgg } : a.content;
|
||||
if (a.type === 'feed') {
|
||||
const base = a.content || {};
|
||||
|
|
@ -688,12 +710,13 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
}
|
||||
|
||||
const tribeInternalTypes = new Set(['tribe-content', 'tribeParliamentCandidature', 'tribeParliamentTerm', 'tribeParliamentProposal', 'tribeParliamentRule', 'tribeParliamentLaw', 'tribeParliamentRevocation']);
|
||||
const hiddenTypes = new Set(['padEntry', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'gameScore']);
|
||||
const hiddenTypes = new Set(['padEntry', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'logPublic', 'gameScore', 'pollVote', 'pollClose', 'pollOpinion', 'curriculum']);
|
||||
const chatThreadItems = await buildChatThreads(ssbClient, idToAction, rootOf, deduped);
|
||||
deduped = deduped.concat(chatThreadItems);
|
||||
const isAllowedTribeActivity = (a) => {
|
||||
if (tribeInternalTypes.has(a.type)) return false;
|
||||
const c = a.content || {};
|
||||
if (a.type === 'poll' && c.chatId) return false;
|
||||
if (c.tribeId) return false;
|
||||
if (a.type === 'tribe') {
|
||||
const isInitial = !c.replaces;
|
||||
|
|
@ -704,8 +727,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
}
|
||||
return true;
|
||||
};
|
||||
const itemVisible = (a) => {
|
||||
if (hiddenTypes.has(a.type)) return false;
|
||||
const itemVisible = (a, opts = {}) => {
|
||||
if (hiddenTypes.has(a.type) && !(opts.allowHidden && opts.allowHidden.has(a.type))) return false;
|
||||
const c = a.content || {};
|
||||
if (c.encryptedPayload) return false;
|
||||
if (a.type === 'pad' && c.status !== 'OPEN') return false;
|
||||
|
|
@ -722,8 +745,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
if (a.type === 'transfer' && String(c.status || '').toUpperCase() === 'UNCONFIRMED' && a.author !== userId && c.from !== userId && c.to !== userId) return false;
|
||||
return true;
|
||||
};
|
||||
const isVisible = (a) => {
|
||||
if (!itemVisible(a)) return false;
|
||||
const isVisible = (a, opts) => {
|
||||
if (!itemVisible(a, opts)) return false;
|
||||
if (a.type === 'post' || a.type === 'opinion') {
|
||||
const c = a.content || {};
|
||||
const ref = (typeof c.root === 'string' && c.root)
|
||||
|
|
@ -742,6 +765,14 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
if (filter === 'mine') out = deduped.filter(a => a.author === userId && isAllowedTribeActivity(a) && isVisible(a));
|
||||
else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff && isAllowedTribeActivity(a) && isVisible(a)) }
|
||||
else if (filter === 'all') out = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a));
|
||||
else if (filter === 'top') {
|
||||
const visible = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a));
|
||||
const perAuthor = new Map();
|
||||
for (const a of visible) perAuthor.set(a.author, (perAuthor.get(a.author) || 0) + 1);
|
||||
out = visible
|
||||
.map(a => ({ ...a, authorActions: perAuthor.get(a.author) || 0 }))
|
||||
.sort((x, y) => (y.authorActions - x.authorActions) || ((y.ts || 0) - (x.ts || 0)));
|
||||
}
|
||||
else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim' || a.type === 'ubiClaim');
|
||||
else if (filter === 'tribe') out = deduped.filter(a => a.type === 'tribe' || String(a.type || '').startsWith('tribe'));
|
||||
else if (filter === 'spread') out = deduped.filter(a => a.type === 'spread');
|
||||
|
|
@ -756,6 +787,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
});
|
||||
else if (filter === 'task')
|
||||
out = deduped.filter(a => a.type === 'task' || a.type === 'taskAssignment');
|
||||
else if (filter === 'votes')
|
||||
out = deduped.filter(a => (a.type === 'votes' || a.type === 'poll') && isAllowedTribeActivity(a) && isVisible(a));
|
||||
else if (filter === 'industry')
|
||||
out = deduped.filter(a => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(a.type) && isVisible(a));
|
||||
else if (filter === 'pad') out = deduped.filter(a => a.type === 'pad' && (a.content || {}).status === 'OPEN');
|
||||
|
|
@ -764,7 +797,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
else if (filter === 'transfer') out = deduped.filter(a => a.type === 'transfer' && isVisible(a));
|
||||
else out = deduped.filter(a => a.type === filter);
|
||||
|
||||
out.sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
||||
if (filter !== 'top') out.sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
||||
return out;
|
||||
})();
|
||||
_feedCacheInflight.set(cacheKey, promise);
|
||||
|
|
|
|||
168
src/models/blog_model.js
Normal file
168
src/models/blog_model.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
const pull = require('../server/node_modules/pull-stream');
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||
|
||||
const OPINION_TYPE = 'blogOpinion';
|
||||
|
||||
module.exports = ({ cooler, isPublic = false }) => {
|
||||
let ssb;
|
||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||
|
||||
const getAllMessages = async (ssbClient) =>
|
||||
new Promise((resolve, reject) => {
|
||||
pull(
|
||||
ssbClient.createLogStream({ limit: logLimit }),
|
||||
pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))
|
||||
);
|
||||
});
|
||||
|
||||
const isRootPost = (c) =>
|
||||
!!c && c.type === 'post' && typeof c.text === 'string' &&
|
||||
!c.root && !c.fork && !c.branch && !c.about && !c.target;
|
||||
|
||||
const buildIndex = (messages) => {
|
||||
const tomb = buildValidatedTombstoneSet(messages);
|
||||
const posts = new Map();
|
||||
const commentsByRoot = new Map();
|
||||
const opinionsByRoot = new Map();
|
||||
|
||||
for (const m of messages) {
|
||||
const v = m && m.value;
|
||||
const c = v && v.content;
|
||||
if (!c || typeof c !== 'object') continue;
|
||||
if (tomb.has(m.key)) continue;
|
||||
|
||||
if (c.type === 'post' && typeof c.text === 'string') {
|
||||
if (isRootPost(c)) {
|
||||
posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c });
|
||||
} else if (typeof c.root === 'string') {
|
||||
commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.type === OPINION_TYPE && typeof c.target === 'string') {
|
||||
const entry = opinionsByRoot.get(c.target) || { counts: {}, voters: [] };
|
||||
if (!entry.voters.includes(v.author)) {
|
||||
entry.voters.push(v.author);
|
||||
entry.counts[c.category] = (entry.counts[c.category] || 0) + 1;
|
||||
}
|
||||
opinionsByRoot.set(c.target, entry);
|
||||
}
|
||||
}
|
||||
|
||||
return { tomb, posts, commentsByRoot, opinionsByRoot };
|
||||
};
|
||||
|
||||
const buildBlog = (node, idx) => {
|
||||
const c = node.c || {};
|
||||
const op = idx.opinionsByRoot.get(node.key) || { counts: {}, voters: [] };
|
||||
return {
|
||||
id: node.key,
|
||||
key: node.key,
|
||||
author: node.author,
|
||||
subject: typeof c.contentWarning === 'string' ? c.contentWarning : '',
|
||||
text: c.text || '',
|
||||
mentions: Array.isArray(c.mentions) ? c.mentions : [],
|
||||
allowComments: c.allowComments !== false,
|
||||
createdAt: new Date(node.ts).toISOString(),
|
||||
commentCount: idx.commentsByRoot.get(node.key) || 0,
|
||||
opinions: op.counts,
|
||||
opinions_inhabitants: op.voters
|
||||
};
|
||||
};
|
||||
|
||||
const collect = async () => {
|
||||
const ssbClient = await openSsb();
|
||||
const messages = await getAllMessages(ssbClient);
|
||||
const idx = buildIndex(messages);
|
||||
const list = [];
|
||||
for (const node of idx.posts.values()) list.push(buildBlog(node, idx));
|
||||
return { list, idx, viewerId: ssbClient.id };
|
||||
};
|
||||
|
||||
const opinionScore = (blog) =>
|
||||
Object.values(blog.opinions || {}).reduce((n, v) => n + (Number(v) || 0), 0);
|
||||
|
||||
return {
|
||||
type: 'post',
|
||||
|
||||
async listAll(filter = 'ALL', opts = {}) {
|
||||
const { list, viewerId } = await collect();
|
||||
const f = String(filter || 'ALL').toUpperCase();
|
||||
const favorites = Array.isArray(opts.favorites) ? new Set(opts.favorites.map(String)) : null;
|
||||
let out = list.slice();
|
||||
|
||||
if (f === 'MINE') out = out.filter(b => String(b.author) === String(viewerId));
|
||||
else if (f === 'FAVORITES') out = favorites ? out.filter(b => favorites.has(String(b.id))) : [];
|
||||
|
||||
const q = String(opts.q || '').trim().toLowerCase();
|
||||
if (q) out = out.filter(b =>
|
||||
b.text.toLowerCase().includes(q) || b.subject.toLowerCase().includes(q));
|
||||
|
||||
if (f === 'TOP') {
|
||||
out.sort((a, b) => (opinionScore(b) + b.commentCount) - (opinionScore(a) + a.commentCount) ||
|
||||
new Date(b.createdAt) - new Date(a.createdAt));
|
||||
} else {
|
||||
out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
async getBlogById(id) {
|
||||
const { list } = await collect();
|
||||
const blog = list.find(b => b.id === id);
|
||||
if (!blog) throw new Error('Blog not found');
|
||||
return blog;
|
||||
},
|
||||
|
||||
async createBlog({ text, subject = '', mentions = [], allowComments = true } = {}) {
|
||||
if (isPublic) throw new Error('Not available in public mode');
|
||||
const body = String(text || '').trim();
|
||||
if (!body) throw new Error('Blog text is required');
|
||||
const ssbClient = await openSsb();
|
||||
const content = {
|
||||
type: 'post',
|
||||
text: body,
|
||||
allowComments: allowComments !== false,
|
||||
...(Array.isArray(mentions) && mentions.length ? { mentions } : {}),
|
||||
...(String(subject || '').trim() ? { contentWarning: String(subject).trim() } : {})
|
||||
};
|
||||
return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg)));
|
||||
},
|
||||
|
||||
async createOpinion(id, category) {
|
||||
const categories = require('../backend/opinion_categories');
|
||||
if (!categories.includes(category)) throw new Error('Invalid opinion category');
|
||||
const ssbClient = await openSsb();
|
||||
const userId = ssbClient.id;
|
||||
const blog = await this.getBlogById(id);
|
||||
if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined');
|
||||
const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() };
|
||||
return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)));
|
||||
},
|
||||
|
||||
async resolveRootId(id) {
|
||||
const ssbClient = await openSsb();
|
||||
const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m)));
|
||||
const c = msg && (msg.content || (msg.value && msg.value.content));
|
||||
if (c && c.type === 'post' && typeof c.root === 'string') return c.root;
|
||||
return id;
|
||||
},
|
||||
|
||||
async blogHrefFor(id) {
|
||||
const ssbClient = await openSsb();
|
||||
const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m)));
|
||||
const c = msg && (msg.content || (msg.value && msg.value.content));
|
||||
if (!c || c.type !== 'post' || typeof c.text !== 'string') return null;
|
||||
if (c.private === true || Array.isArray(c.recps)) return null;
|
||||
if (typeof c.root === 'string') {
|
||||
return `/blogs/${encodeURIComponent(c.root)}#${encodeURIComponent(id)}`;
|
||||
}
|
||||
if (c.fork || c.branch || c.about || c.target) return null;
|
||||
return `/blogs/${encodeURIComponent(id)}`;
|
||||
}
|
||||
};
|
||||
};
|
||||
292
src/models/data_model.js
Normal file
292
src/models/data_model.js
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
const pull = require('../server/node_modules/pull-stream');
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||
|
||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||
|
||||
const norm = (v) => String(v == null ? '' : v).trim().toLowerCase();
|
||||
|
||||
const KINDS = {
|
||||
inhabitants: { type: 'curriculum', href: (id, c) => `/author/${encodeURIComponent(c.author)}` },
|
||||
jobs: { type: 'job', href: (id) => `/jobs/${encodeURIComponent(id)}` },
|
||||
projects: { type: 'project', href: (id) => `/projects/${encodeURIComponent(id)}` },
|
||||
events: { type: 'event', href: (id) => `/events/${encodeURIComponent(id)}` },
|
||||
tribes: { type: 'tribe', href: (id) => `/tribe/${encodeURIComponent(id)}` },
|
||||
market: { type: 'market', href: (id) => `/market/${encodeURIComponent(id)}` },
|
||||
housing: { type: 'housing', href: (id) => `/housing/${encodeURIComponent(id)}` },
|
||||
industry: { type: 'industry', href: (id) => `/industry/${encodeURIComponent(id)}` },
|
||||
tasks: { type: 'task', href: (id) => `/tasks/${encodeURIComponent(id)}` },
|
||||
reports: { type: 'report', href: (id) => `/reports/${encodeURIComponent(id)}` },
|
||||
votes: { type: 'poll', href: (id) => `/polls/${encodeURIComponent(id)}` },
|
||||
audios: { type: 'audio', href: (id) => `/audios/${encodeURIComponent(id)}` },
|
||||
videos: { type: 'video', href: (id) => `/videos/${encodeURIComponent(id)}` },
|
||||
images: { type: 'image', href: (id) => `/images/${encodeURIComponent(id)}` },
|
||||
documents: { type: 'document', href: (id) => `/documents/${encodeURIComponent(id)}` },
|
||||
bookmarks: { type: 'bookmark', href: (id) => `/bookmarks/${encodeURIComponent(id)}` },
|
||||
torrents: { type: 'torrent', href: (id) => `/torrents/${encodeURIComponent(id)}` },
|
||||
chats: { type: 'chat', href: (id) => `/chats/${encodeURIComponent(id)}` },
|
||||
pads: { type: 'pad', href: (id) => `/pads/${encodeURIComponent(id)}` },
|
||||
maps: { type: 'map', href: (id) => `/maps/${encodeURIComponent(id)}` },
|
||||
calendars: { type: 'calendar', href: (id) => `/calendars/${encodeURIComponent(id)}` },
|
||||
forum: { type: 'forum', href: (id) => `/forum/${encodeURIComponent(id)}` }
|
||||
};
|
||||
|
||||
const KIND_BY_TYPE = Object.fromEntries(Object.entries(KINDS).map(([k, v]) => [v.type, k]));
|
||||
|
||||
const cvSkills = (c) => [
|
||||
...(c.personalSkills || []),
|
||||
...(c.oasisSkills || []),
|
||||
...(c.educationalSkills || []),
|
||||
...(c.professionalSkills || [])
|
||||
];
|
||||
|
||||
const PLACEHOLDERS = new Set(['unknown', 'n/a', 'na', 'none', '-', 'other']);
|
||||
|
||||
const TITLE_STOPWORDS = new Set([
|
||||
'the', 'this', 'that', 'and', 'for', 'with', 'from', 'into', 'about', 'new', 'all', 'not', 'are', 'was', 'you', 'your', 'our',
|
||||
'los', 'las', 'del', 'una', 'unos', 'unas', 'este', 'esta', 'esto', 'que', 'con', 'para', 'por', 'sin', 'sobre', 'bajo', 'mas', 'más'
|
||||
]);
|
||||
|
||||
const titleTerms = (c) => String(c.title || c.name || c.question || c.concept || '')
|
||||
.toLowerCase()
|
||||
.split(/[^\p{L}\p{N}]+/u)
|
||||
.filter(w => w.length >= 3 && !TITLE_STOPWORDS.has(w) && !/^\d+$/.test(w));
|
||||
|
||||
const termsOf = (kind, c) => {
|
||||
const out = [];
|
||||
if (kind === 'inhabitants') {
|
||||
out.push(...cvSkills(c));
|
||||
if (c.languages) out.push(...String(c.languages).split(/[,;]/));
|
||||
} else if (kind === 'jobs') {
|
||||
out.push(...(c.tasks || []), ...(c.tags || []), c.job_type, c.location);
|
||||
} else if (kind === 'industry') {
|
||||
out.push(...(c.tags || []), c.sector, ...(c.skills || []));
|
||||
} else {
|
||||
out.push(...(c.tags || []));
|
||||
if (c.category) out.push(c.category);
|
||||
}
|
||||
return Array.from(new Set(out.map(norm).filter(t => t && !PLACEHOLDERS.has(t))));
|
||||
};
|
||||
|
||||
const titleOf = (kind, c, author) => {
|
||||
if (kind === 'inhabitants') return c.name || author || '';
|
||||
return c.title || c.name || c.question || c.concept || '';
|
||||
};
|
||||
|
||||
const jaccard = (a, b) => {
|
||||
if (!a.size || !b.size) return { score: 0, common: [] };
|
||||
const common = [...b].filter(x => a.has(x));
|
||||
const union = a.size + b.size - common.length;
|
||||
return { score: union > 0 ? common.length / union : 0, common };
|
||||
};
|
||||
|
||||
module.exports = ({ cooler }) => {
|
||||
let ssb;
|
||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||
|
||||
const getAllMessages = async (ssbClient) =>
|
||||
new Promise((resolve, reject) => {
|
||||
pull(
|
||||
ssbClient.createLogStream({ limit: logLimit }),
|
||||
pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))
|
||||
);
|
||||
});
|
||||
|
||||
const buildGraph = async () => {
|
||||
const ssbClient = await openSsb();
|
||||
const viewerId = ssbClient.id;
|
||||
const messages = await getAllMessages(ssbClient);
|
||||
const tomb = buildValidatedTombstoneSet(messages);
|
||||
|
||||
const latestByKey = new Map();
|
||||
const replaced = new Set();
|
||||
for (const m of messages) {
|
||||
const v = m && m.value;
|
||||
const c = v && v.content;
|
||||
if (!c || typeof c !== 'object' || !c.type) continue;
|
||||
if (tomb.has(m.key)) continue;
|
||||
if (c.encryptedPayload || c.encryptedQuestion) continue;
|
||||
if (c.tribeId && c.type !== 'tribe') continue;
|
||||
const kind = KIND_BY_TYPE[c.type];
|
||||
if (!kind) continue;
|
||||
if (typeof c.replaces === 'string') replaced.add(c.replaces);
|
||||
latestByKey.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, kind, c });
|
||||
}
|
||||
|
||||
const nodes = [];
|
||||
const byAuthorCv = new Map();
|
||||
for (const node of latestByKey.values()) {
|
||||
if (replaced.has(node.key)) continue;
|
||||
const coreTerms = termsOf(node.kind, node.c);
|
||||
const extra = node.kind === 'inhabitants' ? [] : titleTerms(node.c).map(norm).filter(t => t && !PLACEHOLDERS.has(t));
|
||||
const terms = Array.from(new Set([...coreTerms, ...extra]));
|
||||
if (!terms.length) continue;
|
||||
const entry = {
|
||||
id: node.key,
|
||||
kind: node.kind,
|
||||
author: node.c.author || node.author,
|
||||
title: titleOf(node.kind, node.c, node.c.author || node.author),
|
||||
terms,
|
||||
termSet: new Set(terms),
|
||||
coreTermSet: new Set(coreTerms),
|
||||
ts: node.ts,
|
||||
createdAt: node.c.createdAt || new Date(node.ts).toISOString(),
|
||||
href: KINDS[node.kind].href(node.key, node.c)
|
||||
};
|
||||
if (node.kind === 'inhabitants') {
|
||||
const prev = byAuthorCv.get(entry.author);
|
||||
if (prev && prev.ts >= entry.ts) continue;
|
||||
byAuthorCv.set(entry.author, entry);
|
||||
continue;
|
||||
}
|
||||
nodes.push(entry);
|
||||
}
|
||||
for (const cv of byAuthorCv.values()) nodes.push(cv);
|
||||
|
||||
return { viewerId, nodes, cvByAuthor: byAuthorCv };
|
||||
};
|
||||
|
||||
const MAX_ENTITIES = 400;
|
||||
const MAX_PAIRS = 300;
|
||||
|
||||
const strip = (n) => ({
|
||||
id: n.id, kind: n.kind, author: n.author, title: n.title,
|
||||
href: n.href, createdAt: n.createdAt, ts: n.ts
|
||||
});
|
||||
|
||||
return {
|
||||
KINDS: Object.keys(KINDS),
|
||||
|
||||
async listMatches(filter = 'ALL', opts = {}) {
|
||||
const { viewerId, nodes, cvByAuthor } = await buildGraph();
|
||||
const use = nodes.slice(0, MAX_ENTITIES);
|
||||
const f = String(filter || 'ALL').toUpperCase();
|
||||
|
||||
const myTermSet = new Set();
|
||||
const mineCv = cvByAuthor.get(viewerId);
|
||||
if (mineCv) for (const t of mineCv.terms) myTermSet.add(t);
|
||||
for (const n of use) {
|
||||
if (String(n.author) === String(viewerId)) for (const t of n.terms) myTermSet.add(t);
|
||||
}
|
||||
|
||||
const df = new Map();
|
||||
for (const n of use) for (const t of n.termSet) df.set(t, (df.get(t) || 0) + 1);
|
||||
const total = use.length || 1;
|
||||
const weightOf = (t) => Math.log(1 + total / (df.get(t) || 1));
|
||||
|
||||
let out = [];
|
||||
for (const n of use) {
|
||||
if (String(n.author) === String(viewerId)) continue;
|
||||
const common = [...n.termSet].filter(t => myTermSet.has(t));
|
||||
if (!common.length) continue;
|
||||
let commonW = 0;
|
||||
for (const t of common) commonW += weightOf(t);
|
||||
let itemW = 0;
|
||||
for (const t of n.termSet) itemW += weightOf(t);
|
||||
const score = itemW > 0 ? Math.min(1, commonW / itemW) : 0;
|
||||
if (score <= 0) continue;
|
||||
common.sort((x, y) => weightOf(y) - weightOf(x) || x.localeCompare(y));
|
||||
out.push({ ...strip(n), score, common, connections: common.length });
|
||||
}
|
||||
|
||||
if (f === 'RECENT') {
|
||||
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||
out = out.filter(s => s.ts >= cutoff);
|
||||
} else if (f !== 'ALL' && f !== 'TOP') {
|
||||
const kind = f.toLowerCase();
|
||||
if (KINDS[kind]) out = out.filter(s => s.kind === kind);
|
||||
}
|
||||
|
||||
const q = norm(opts.q);
|
||||
if (q) out = out.filter(s => norm(s.title).includes(q) || s.common.some(t => t.includes(q)));
|
||||
|
||||
if (f === 'RECENT') out.sort((x, y) => y.ts - x.ts || y.score - x.score);
|
||||
else out.sort((x, y) => y.score - x.score || y.ts - x.ts);
|
||||
|
||||
return {
|
||||
matches: out.slice(0, MAX_PAIRS),
|
||||
total: out.length,
|
||||
hasProfile: myTermSet.size > 0,
|
||||
myTerms: [...myTermSet]
|
||||
};
|
||||
},
|
||||
|
||||
async jobMatchesFor(viewerId, { minScore = 0.8 } = {}) {
|
||||
const { nodes, cvByAuthor } = await buildGraph();
|
||||
const mine = cvByAuthor.get(viewerId);
|
||||
if (!mine) return [];
|
||||
return nodes
|
||||
.filter(n => n.kind === 'jobs' && String(n.author) !== String(viewerId))
|
||||
.map(n => {
|
||||
const { score, common } = jaccard(mine.termSet, n.coreTermSet || n.termSet);
|
||||
return { id: n.id, title: n.title, author: n.author, href: n.href, score, common };
|
||||
})
|
||||
.filter(m => m.score >= minScore)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
},
|
||||
|
||||
async cohesion() {
|
||||
const { cvByAuthor, nodes } = await buildGraph();
|
||||
const use = nodes.slice(0, MAX_ENTITIES);
|
||||
let comparisons = 0;
|
||||
let sum = 0;
|
||||
let connectedPairs = 0;
|
||||
const linkedEntities = new Set();
|
||||
for (let i = 0; i < use.length; i++) {
|
||||
for (let j = i + 1; j < use.length; j++) {
|
||||
const a = use[i], b = use[j];
|
||||
if (a.kind === 'inhabitants' && b.kind === 'inhabitants' && a.author === b.author) continue;
|
||||
const { score } = jaccard(a.termSet, b.termSet);
|
||||
comparisons += 1;
|
||||
sum += score;
|
||||
if (score > 0) { connectedPairs += 1; linkedEntities.add(a.id); linkedEntities.add(b.id); }
|
||||
}
|
||||
}
|
||||
const coefficient = comparisons > 0 ? sum / comparisons : 0;
|
||||
|
||||
const people = [...cvByAuthor.values()];
|
||||
const linked = new Set();
|
||||
let cvComparisons = 0;
|
||||
let cvSum = 0;
|
||||
for (let i = 0; i < people.length; i++) {
|
||||
for (let j = i + 1; j < people.length; j++) {
|
||||
const { score } = jaccard(people[i].termSet, people[j].termSet);
|
||||
cvComparisons += 1;
|
||||
cvSum += score;
|
||||
if (score > 0) { linked.add(people[i].author); linked.add(people[j].author); }
|
||||
}
|
||||
}
|
||||
const cvCoefficient = cvComparisons > 0 ? cvSum / cvComparisons : 0;
|
||||
|
||||
const skillSet = new Set();
|
||||
for (const p of people) for (const t of p.terms) skillSet.add(t);
|
||||
|
||||
const termCount = new Map();
|
||||
for (const n of nodes) for (const t of n.terms) termCount.set(t, (termCount.get(t) || 0) + 1);
|
||||
const topTerms = [...termCount.entries()]
|
||||
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
.slice(0, 12)
|
||||
.map(([term, count]) => ({ term, count }));
|
||||
|
||||
const perKind = {};
|
||||
for (const n of nodes) perKind[n.kind] = (perKind[n.kind] || 0) + 1;
|
||||
|
||||
return {
|
||||
coefficient,
|
||||
percent: Math.round(coefficient * 1000) / 10,
|
||||
comparisons,
|
||||
pairs: connectedPairs,
|
||||
entities: use.length,
|
||||
distinctTerms: termCount.size,
|
||||
topTerms,
|
||||
perKind,
|
||||
people: people.length,
|
||||
skills: skillSet.size,
|
||||
connected: linkedEntities.size,
|
||||
isolated: Math.max(0, use.length - linkedEntities.size),
|
||||
cvCoefficient,
|
||||
cvPercent: Math.round(cvCoefficient * 1000) / 10
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -130,9 +130,9 @@ module.exports = ({ cooler }) => {
|
|||
return filterInactive(users);
|
||||
}
|
||||
|
||||
if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP ECO') {
|
||||
if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP INACTIVITY' || filter === 'TOP ECO') {
|
||||
let users = await listAllBase(ssbClient);
|
||||
if (filter !== 'TOP ACTIVITY') {
|
||||
if (filter !== 'TOP ACTIVITY' && filter !== 'TOP INACTIVITY') {
|
||||
users = filterInactive(users);
|
||||
}
|
||||
if (search) {
|
||||
|
|
@ -170,6 +170,7 @@ module.exports = ({ cooler }) => {
|
|||
}));
|
||||
if (filter === 'TOP KARMA') return withMetrics.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0));
|
||||
if (filter === 'TOP ACTIVITY') return withMetrics.sort((a, b) => (b.lastActivityTs || 0) - (a.lastActivityTs || 0));
|
||||
if (filter === 'TOP INACTIVITY') return withMetrics.sort((a, b) => (a.lastActivityTs || 0) - (b.lastActivityTs || 0));
|
||||
if (filter === 'TOP ECO') return withMetrics.sort((a, b) => (b.ecoScore || 0) - (a.ecoScore || 0));
|
||||
return withMetrics;
|
||||
}
|
||||
|
|
@ -254,7 +255,7 @@ module.exports = ({ cooler }) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (filter === 'CVs' || filter === 'MATCHSKILLS') {
|
||||
if (filter === 'CVs') {
|
||||
const records = await new Promise((res, rej) => {
|
||||
pull(
|
||||
ssbClient.createLogStream({ limit: logLimit, reverse: true}),
|
||||
|
|
@ -295,54 +296,13 @@ module.exports = ({ cooler }) => {
|
|||
return out;
|
||||
}
|
||||
|
||||
if (filter === 'MATCHSKILLS') {
|
||||
let base = await Promise.all(cvs.map(async c => {
|
||||
const photo = await fetchUserImageUrl(c.author, 256);
|
||||
const lastActivityTs = await getLastActivityTimestamp(c.author);
|
||||
const { bucket, range } = bucketLastActivity(lastActivityTs);
|
||||
const norm = this._normalizeCurriculum(c, photo);
|
||||
const karmaScore = await getLastKarmaScore(c.author).catch(() => 0);
|
||||
return { ...norm, lastActivityTs, lastActivityBucket: bucket, lastActivityRange: range, karmaScore };
|
||||
}));
|
||||
base = filterInactive(base);
|
||||
const mecv = await this.getCVByUserId();
|
||||
const userSkills = Array.from(new Set(
|
||||
(mecv
|
||||
? [
|
||||
...(mecv.personalSkills || []),
|
||||
...(mecv.oasisSkills || []),
|
||||
...(mecv.educationalSkills || []),
|
||||
...(mecv.professionalSkills || [])
|
||||
]
|
||||
: []).map(s => String(s || '').toLowerCase()).filter(Boolean)
|
||||
));
|
||||
if (!userSkills.length) return [];
|
||||
const userSet = new Set(userSkills);
|
||||
const matches = base.map(c => {
|
||||
if (c.id === userId) return null;
|
||||
const theirSkillsRaw = (c.skills || []).map(s => String(s || '').toLowerCase()).filter(Boolean);
|
||||
const theirSet = new Set(theirSkillsRaw);
|
||||
const common = Array.from(theirSet).filter(s => userSet.has(s));
|
||||
if (!common.length) return null;
|
||||
const unionSize = userSet.size + theirSet.size - common.length;
|
||||
const matchScore = unionSize > 0 ? common.length / unionSize : 0;
|
||||
const matchCoverage = userSet.size > 0 ? common.length / userSet.size : 0;
|
||||
return { ...c, commonSkills: common, matchScore, matchCoverage };
|
||||
}).filter(Boolean);
|
||||
return matches.sort((a, b) =>
|
||||
(b.matchScore - a.matchScore) ||
|
||||
(b.commonSkills.length - a.commonSkills.length) ||
|
||||
((b.karmaScore || 0) - (a.karmaScore || 0)) ||
|
||||
((b.lastActivityTs || 0) - (a.lastActivityTs || 0))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
_normalizeCurriculum(c, photoUrl) {
|
||||
const photo = photoUrl || toImageUrl(c.photo, 256);
|
||||
const photo = c.photo ? toImageUrl(c.photo, 256) : (photoUrl || toImageUrl(null, 256));
|
||||
return {
|
||||
id: c.author,
|
||||
name: c.name,
|
||||
|
|
@ -358,6 +318,8 @@ module.exports = ({ cooler }) => {
|
|||
languages: typeof c.languages === 'string'
|
||||
? c.languages.split(',').map(x => x.trim())
|
||||
: Array.isArray(c.languages) ? c.languages : [],
|
||||
status: c.status,
|
||||
preferences: c.preferences,
|
||||
createdAt: c.createdAt
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -568,10 +568,14 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
});
|
||||
}
|
||||
|
||||
function wallIsPublic(houseKey, { isGoverning = false } = {}) {
|
||||
return houseKey === 'academia' || isGoverning === true;
|
||||
}
|
||||
|
||||
async function listHousePosts(houseKey, { viewerHouse = null, isGoverning = false } = {}) {
|
||||
if (!VALID_KEY(houseKey)) return [];
|
||||
const viewerIsMember = viewerHouse === houseKey;
|
||||
if (!viewerIsMember && !isGoverning) return [];
|
||||
if (!viewerIsMember && !wallIsPublic(houseKey, { isGoverning })) return [];
|
||||
const client = await openSsb();
|
||||
const memberships = await listAllMemberships();
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -912,6 +916,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
return {
|
||||
HOUSES,
|
||||
HOUSE_KEYS,
|
||||
wallIsPublic,
|
||||
TEST_COOLDOWN_MS,
|
||||
TEST_QUESTIONS_COUNT,
|
||||
PROFILE_QUESTIONS,
|
||||
|
|
|
|||
171
src/models/mentions_model.js
Normal file
171
src/models/mentions_model.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
const fs = require('fs');
|
||||
const pull = require('../server/node_modules/pull-stream');
|
||||
|
||||
const SEEN_FILE = 'oasis-mentions-seen';
|
||||
|
||||
const seenPath = () => {
|
||||
try { return require('../server/ssb_config').statePath(SEEN_FILE); } catch (_) { return null; }
|
||||
};
|
||||
|
||||
const MAX_SEEN_KEYS = 500;
|
||||
|
||||
const readSeen = () => {
|
||||
const p = seenPath();
|
||||
if (!p) return { ts: 0, keys: [] };
|
||||
let raw = '';
|
||||
try { raw = fs.readFileSync(p, 'utf8').trim(); } catch (_) { return { ts: 0, keys: [] }; }
|
||||
if (!raw) return { ts: 0, keys: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return { ts: Number(parsed.ts) || 0, keys: Array.isArray(parsed.keys) ? parsed.keys : [] };
|
||||
}
|
||||
} catch (_) {}
|
||||
return { ts: Number(raw) || 0, keys: [] };
|
||||
};
|
||||
|
||||
const writeSeen = (keys, ts) => {
|
||||
const p = seenPath();
|
||||
if (!p) return false;
|
||||
const trimmed = (Array.isArray(keys) ? keys : []).filter(Boolean).slice(0, MAX_SEEN_KEYS);
|
||||
try { fs.writeFileSync(p, JSON.stringify({ ts: Number(ts) || Date.now(), keys: trimmed })); return true; } catch (_) { return false; }
|
||||
};
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||
|
||||
const TEXT_FIELDS = ['text', 'description', 'title', 'concept', 'question', 'rules', 'message', 'body', 'subject'];
|
||||
|
||||
const SKIP_TYPES = new Set([
|
||||
'about', 'contact', 'vote', 'pub', 'tombstone', 'karmaScore',
|
||||
'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-key-distrib'
|
||||
]);
|
||||
|
||||
module.exports = ({ cooler }) => {
|
||||
let ssb;
|
||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||
|
||||
const getAllMessages = async (ssbClient) =>
|
||||
new Promise((resolve, reject) => {
|
||||
pull(
|
||||
ssbClient.createLogStream({ limit: logLimit }),
|
||||
pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))
|
||||
);
|
||||
});
|
||||
|
||||
const mentionsList = (content, feedId) => {
|
||||
const m = content && content.mentions;
|
||||
if (!m) return false;
|
||||
const values = Array.isArray(m) ? m : (typeof m === 'object' ? Object.values(m).flat() : []);
|
||||
return values.some(v => v && (v.link === feedId || v === feedId));
|
||||
};
|
||||
|
||||
const mentionsText = (content, feedId) => {
|
||||
const bare = feedId.startsWith('@') ? feedId.slice(1) : feedId;
|
||||
for (const field of TEXT_FIELDS) {
|
||||
const v = content[field];
|
||||
if (typeof v === 'string' && (v.includes(feedId) || v.includes(bare))) return true;
|
||||
}
|
||||
if (Array.isArray(content.tags)) {
|
||||
if (content.tags.some(t => typeof t === 'string' && (t.includes(feedId) || t.includes(bare)))) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const snippet = (content) => {
|
||||
for (const field of TEXT_FIELDS) {
|
||||
const v = content[field];
|
||||
if (typeof v === 'string' && v.trim()) return v.trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const titleOf = (content) => {
|
||||
for (const field of ['title', 'concept', 'question', 'subject']) {
|
||||
const v = content[field];
|
||||
if (typeof v === 'string' && v.trim()) return v.trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return {
|
||||
async listMentions(filter = 'ALL', opts = {}) {
|
||||
const ssbClient = await openSsb();
|
||||
const myFeedId = ssbClient.id;
|
||||
const messages = await getAllMessages(ssbClient);
|
||||
const tomb = buildValidatedTombstoneSet(messages);
|
||||
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const replaced = new Set();
|
||||
for (const m of messages) {
|
||||
const v = m && m.value;
|
||||
const c = v && v.content;
|
||||
if (!c || typeof c !== 'object' || !c.type) continue;
|
||||
if (tomb.has(m.key)) continue;
|
||||
if (SKIP_TYPES.has(c.type)) continue;
|
||||
if (c.encryptedPayload || c.encryptedQuestion) continue;
|
||||
if (Array.isArray(c.recps) || Array.isArray(v.content && v.content.recps)) continue;
|
||||
if (c.tribeId && c.type !== 'tribe') continue;
|
||||
if (String(v.author) === String(myFeedId)) continue;
|
||||
if (typeof c.replaces === 'string') replaced.add(c.replaces);
|
||||
if (!mentionsList(c, myFeedId) && !mentionsText(c, myFeedId)) continue;
|
||||
if (seen.has(m.key)) continue;
|
||||
seen.add(m.key);
|
||||
out.push({
|
||||
id: m.key,
|
||||
key: m.key,
|
||||
author: v.author,
|
||||
type: c.type,
|
||||
title: titleOf(c),
|
||||
text: snippet(c),
|
||||
content: c,
|
||||
createdAt: new Date(v.timestamp || m.timestamp || 0).toISOString(),
|
||||
ts: v.timestamp || m.timestamp || 0
|
||||
});
|
||||
}
|
||||
|
||||
const fresh = out.filter(x => !replaced.has(x.key));
|
||||
const q = String(opts.q || '').trim().toLowerCase();
|
||||
let list = q
|
||||
? fresh.filter(x => x.text.toLowerCase().includes(q) || x.title.toLowerCase().includes(q) || x.type.toLowerCase().includes(q))
|
||||
: fresh;
|
||||
|
||||
const f = String(filter || 'ALL').toUpperCase();
|
||||
if (f !== 'ALL' && f !== 'RECENT') list = list.filter(x => x.type === filter);
|
||||
|
||||
list.sort((a, b) => b.ts - a.ts);
|
||||
return list;
|
||||
},
|
||||
|
||||
seenAt: () => readSeen().ts,
|
||||
|
||||
markSeen(list) {
|
||||
const items = Array.isArray(list) ? list : [];
|
||||
const keys = items.map(x => x && x.key).filter(Boolean);
|
||||
const newest = items.reduce((max, x) => Math.max(max, Number(x && x.ts) || 0), 0);
|
||||
return writeSeen(keys, newest || Date.now());
|
||||
},
|
||||
|
||||
unseenOf(list) {
|
||||
const seen = readSeen();
|
||||
const known = new Set(seen.keys);
|
||||
const items = Array.isArray(list) ? list : [];
|
||||
if (known.size) return items.filter(x => x && !known.has(x.key));
|
||||
if (seen.ts) return items.filter(x => x && (Number(x.ts) || 0) > seen.ts);
|
||||
return items;
|
||||
},
|
||||
|
||||
async countUnseen() {
|
||||
return this.unseenOf(await this.listMentions('ALL')).length;
|
||||
},
|
||||
|
||||
async countTypes(list) {
|
||||
const counts = {};
|
||||
for (const item of (Array.isArray(list) ? list : [])) {
|
||||
counts[item.type] = (counts[item.type] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -546,7 +546,7 @@ module.exports = ({ cooler }) => {
|
|||
return publishReplace(ssbClient, project.id, patch)
|
||||
},
|
||||
|
||||
async listProjects(filter) {
|
||||
async listProjects(filter, opts = {}) {
|
||||
const ssbClient = await openSsb()
|
||||
const currentUserId = ssbClient.id
|
||||
const idx = buildProjectIndex(await getAllMsgs(ssbClient))
|
||||
|
|
@ -580,6 +580,15 @@ module.exports = ({ cooler }) => {
|
|||
else if (F === "RECENT") list = list.filter((p) => p && moment(p.createdAt).isAfter(moment().subtract(24, "hours")))
|
||||
else if (F === "FOLLOWING") list = list.filter((p) => Array.isArray(p.followers) && p.followers.includes(currentUserId))
|
||||
|
||||
const q = norm(opts.q)
|
||||
if (q) {
|
||||
list = list.filter((p) =>
|
||||
norm(p.title).includes(q) ||
|
||||
norm(p.description).includes(q) ||
|
||||
(Array.isArray(p.tags) && p.tags.some(t => norm(t).includes(q)))
|
||||
)
|
||||
}
|
||||
|
||||
if (F === "TOP") {
|
||||
list.sort((a, b) => (parseFloat(b.pledged || 0) / (parseFloat(b.goal || 1))) - (parseFloat(a.pledged || 0) / (parseFloat(a.goal || 1))))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
|||
'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed',
|
||||
'votes', 'report', 'task', 'event', 'bookmark', 'document',
|
||||
'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim',
|
||||
'project', 'job', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad'
|
||||
'project', 'job', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'poll'
|
||||
];
|
||||
|
||||
const getRelevantFields = (type, content) => {
|
||||
|
|
@ -60,7 +60,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
|||
case 'audio':
|
||||
return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])];
|
||||
case 'image':
|
||||
return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.meme];
|
||||
return [content?.url, content?.title, content?.description, ...(content?.tags || [])];
|
||||
case 'video':
|
||||
return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])];
|
||||
case 'document':
|
||||
|
|
@ -85,6 +85,12 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
|||
return [content?.amount, content?.epochId, content?.allocationId, content?.txid];
|
||||
case 'project':
|
||||
return [content?.title, content?.status, content?.progress, content?.goal, content?.pledged, content?.deadline, (content?.followers || []).length, (content?.backers || []).length, (content?.milestones || []).length, content?.bounty, content?.bountyAmount, content?.bounty_currency, content?.activity?.kind, content?.activityActor];
|
||||
case 'poll':
|
||||
return content?.encryptedQuestion
|
||||
? []
|
||||
: [content?.question, ...(content?.options || []), ...(content?.tags || [])];
|
||||
case 'housing':
|
||||
return [content?.title, content?.housing_type, content?.property_type, content?.place, content?.price, content?.rooms, content?.size, content?.capacity, content?.status];
|
||||
case 'job':
|
||||
return [content?.title, content?.job_type, ...(content?.tasks || []), content?.location, content?.vacants, content?.salary, content?.status, (content?.subscribers || []).length];
|
||||
case 'industry':
|
||||
|
|
@ -232,6 +238,17 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
|||
].join('|');
|
||||
}
|
||||
|
||||
if (t === 'housing') {
|
||||
return [
|
||||
'housing',
|
||||
author,
|
||||
norm(c.title),
|
||||
norm(c.place),
|
||||
norm(c.price),
|
||||
norm(c.housing_type)
|
||||
].join('|');
|
||||
}
|
||||
|
||||
if (t === 'job') {
|
||||
return [
|
||||
'job',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
|||
const norm = (v) => String(v == null ? '' : v).trim().toLowerCase();
|
||||
|
||||
const normalizeTag = (tag) => String(tag == null ? '' : tag).trim().replace(/^#/, '');
|
||||
const tagKey = (tag) => normalizeTag(tag).toLowerCase();
|
||||
const tagKey = (tag) => {
|
||||
const k = normalizeTag(tag).toLowerCase();
|
||||
return k.includes(':') ? '' : k;
|
||||
};
|
||||
|
||||
const getDedupeKey = (msg) => {
|
||||
const c = msg?.value?.content || {};
|
||||
|
|
@ -102,7 +105,7 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
|||
};
|
||||
|
||||
return {
|
||||
async listTags(filter = 'all') {
|
||||
async listTags(filter = 'all', search = '') {
|
||||
const ssbClient = await openSsb();
|
||||
|
||||
const messages = await new Promise((resolve, reject) => {
|
||||
|
|
@ -158,21 +161,34 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
|||
if (c.type === 'task' && String(c.isPublic).toUpperCase() === 'PRIVATE') return false;
|
||||
if ((c.type === 'chat' || c.type === 'pad' || c.type === 'map' || c.type === 'calendar') && c.status === 'INVITE-ONLY' && c.author !== viewerId && !(Array.isArray(c.members) && c.members.includes(viewerId))) return false;
|
||||
if (c.type === 'shop' && c.visibility === 'CLOSED') return false;
|
||||
if (c.type === 'poll' && c.chatId) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
filtered = dedupeKeepLatest(filtered);
|
||||
|
||||
const counts = new Map();
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
const bump = (k, display, { mine, ts }) => {
|
||||
const prev = counts.get(k);
|
||||
if (!prev) counts.set(k, { name: display, count: 1, mine: mine ? 1 : 0, lastTs: ts || 0 });
|
||||
else counts.set(k, {
|
||||
name: prev.name || display,
|
||||
count: prev.count + 1,
|
||||
mine: prev.mine + (mine ? 1 : 0),
|
||||
lastTs: Math.max(prev.lastTs || 0, ts || 0)
|
||||
});
|
||||
};
|
||||
|
||||
for (const record of filtered) {
|
||||
const tagsArr = record?.value?.content?.tags || [];
|
||||
const uniqueTags = new Set(tagsArr.map(tagKey).filter(Boolean));
|
||||
const mine = String(record?.value?.author || '') === String(viewerId);
|
||||
const ts = Number(record?.value?.timestamp || record?.timestamp || 0);
|
||||
for (const k of uniqueTags) {
|
||||
const display = normalizeTag(tagsArr.find(t => tagKey(t) === k) || k) || k;
|
||||
const prev = counts.get(k);
|
||||
if (!prev) counts.set(k, { name: display, count: 1 });
|
||||
else counts.set(k, { name: prev.name || display, count: prev.count + 1 });
|
||||
bump(k, display, { mine, ts });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,16 +202,22 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
|||
const uniquePadTags = new Set(pad.tags.map(tagKey).filter(Boolean));
|
||||
for (const k of uniquePadTags) {
|
||||
const display = normalizeTag(pad.tags.find(t => tagKey(t) === k) || k) || k;
|
||||
const prev = counts.get(k);
|
||||
if (!prev) counts.set(k, { name: display, count: 1 });
|
||||
else counts.set(k, { name: prev.name || display, count: prev.count + 1 });
|
||||
bump(k, display, { mine: String(pad.author || '') === String(viewerId), ts: Number(pad.createdAt ? new Date(pad.createdAt).getTime() : 0) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tags = Array.from(counts.values());
|
||||
|
||||
if (filter === 'top') {
|
||||
const q = String(search || '').trim().toLowerCase().replace(/^#/, '');
|
||||
if (q) tags = tags.filter(t => String(t.name || '').toLowerCase().includes(q));
|
||||
|
||||
if (filter === 'mine') {
|
||||
tags = tags.filter(t => (t.mine || 0) > 0).sort((a, b) => b.mine - a.mine || a.name.localeCompare(b.name));
|
||||
} else if (filter === 'recent') {
|
||||
const since = Date.now() - 7 * DAY;
|
||||
tags = tags.filter(t => (t.lastTs || 0) >= since).sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0));
|
||||
} else if (filter === 'top') {
|
||||
tags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
||||
} else if (filter === 'cloud') {
|
||||
const max = Math.max(...tags.map(t => t.count), 1);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module.exports = ({ cooler }) => {
|
|||
};
|
||||
|
||||
const types = [
|
||||
'bookmark', 'votes', 'feed',
|
||||
'bookmark', 'votes', 'poll', 'feed',
|
||||
'image', 'audio', 'video', 'document', 'torrent', 'transfer',
|
||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct'
|
||||
];
|
||||
|
|
@ -141,7 +141,8 @@ module.exports = ({ cooler }) => {
|
|||
case 'feed':
|
||||
return `feed::${(c.text || '').replace(/\s+/g, ' ').trim()}`;
|
||||
case 'votes':
|
||||
return `votes::${(c.question || '').replace(/\s+/g, ' ').trim()}`;
|
||||
case 'poll':
|
||||
return `${c.type}::${(c.question || '').replace(/\s+/g, ' ').trim()}`;
|
||||
case 'transfer':
|
||||
return `transfer::${(c.concept || '')}|${c.amount || ''}|${c.from || ''}|${c.to || ''}|${c.deadline || ''}`;
|
||||
default:
|
||||
|
|
|
|||
104
src/models/workflows_model.js
Normal file
104
src/models/workflows_model.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
const ALL_MODULES = [
|
||||
'agenda', 'ai', 'aiNav', 'audios', 'banking', 'blogs', 'bookmarks', 'calendars', 'chats', 'cipher',
|
||||
'courts', 'dev', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'graphos',
|
||||
'housing', 'images', 'industry', 'invites', 'jobs', 'larp', 'legacy', 'logs', 'maps', 'market',
|
||||
'melody', 'opinions', 'pads', 'parliament', 'pixelia', 'polls', 'projects', 'reports', 'shops',
|
||||
'tags', 'tasks', 'torrents', 'transfers', 'trending', 'tribes', 'videos', 'votes', 'wallet'
|
||||
];
|
||||
|
||||
const NETWORK = ['feed', 'blogs', 'tags', 'trending', 'opinions', 'pads', 'forum', 'maps', 'chats'];
|
||||
const MEDIA = ['audios', 'bookmarks', 'docs', 'images', 'torrents', 'videos'];
|
||||
const OFFICE = ['agenda', 'calendars', 'events', 'tasks', 'reports', 'favorites'];
|
||||
const GOVERNANCE = ['tribes', 'larp', 'votes', 'polls', 'parliament', 'courts', 'logs'];
|
||||
const ECONOMY = ['banking', 'wallet', 'transfers', 'market', 'housing', 'jobs', 'shops', 'industry', 'projects'];
|
||||
|
||||
const MOBILE_MODULES = [
|
||||
'agenda', 'favorites', 'wallet', 'tribes', 'larp', 'votes', 'polls', 'events', 'calendars', 'tasks',
|
||||
'reports', 'banking', 'market', 'housing', 'jobs', 'shops', 'transfers', 'cipher', 'invites',
|
||||
'games', 'audios', 'bookmarks', 'docs', 'images', ...NETWORK
|
||||
];
|
||||
|
||||
const WORKFLOWS = [
|
||||
{
|
||||
key: 'default',
|
||||
theme: 'Dark-SNH',
|
||||
homePage: 'activity',
|
||||
modules: ALL_MODULES.filter(m => m !== 'aiNav')
|
||||
},
|
||||
{
|
||||
key: 'jobs',
|
||||
theme: 'Clear-SNH',
|
||||
homePage: 'activity',
|
||||
modules: [...OFFICE, 'jobs', 'projects', 'industry', 'market', 'shops', 'banking', 'wallet', 'transfers',
|
||||
'forum', 'chats', 'pads', 'feed', 'tags', 'trending', 'docs', 'invites']
|
||||
},
|
||||
{
|
||||
key: 'ruling',
|
||||
theme: 'Purple-SNH',
|
||||
homePage: 'activity',
|
||||
modules: [...GOVERNANCE, ...OFFICE, 'forum', 'pads', 'chats', 'feed', 'blogs', 'opinions', 'tags', 'trending', 'docs', 'invites']
|
||||
},
|
||||
{
|
||||
key: 'politics',
|
||||
theme: 'Dark-SNH',
|
||||
homePage: 'trending',
|
||||
modules: ['parliament', 'courts', 'votes', 'polls', 'reports', 'logs', 'forum', 'opinions', 'blogs',
|
||||
'feed', 'tags', 'trending', 'pads', 'chats', 'fediverse', 'tribes', 'events', 'agenda']
|
||||
},
|
||||
{
|
||||
key: 'social',
|
||||
theme: 'Clear-SNH',
|
||||
homePage: 'feed',
|
||||
modules: [...NETWORK, ...MEDIA, 'events', 'calendars', 'agenda', 'favorites', 'games', 'pixelia',
|
||||
'melody', 'tribes', 'fediverse', 'invites', 'polls']
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
theme: 'Dark-SNH',
|
||||
homePage: 'activity',
|
||||
modules: [...ECONOMY, ...OFFICE, 'forum', 'chats', 'pads', 'docs', 'feed', 'tags', 'trending', 'invites', 'cipher']
|
||||
},
|
||||
{
|
||||
key: 'night',
|
||||
theme: 'Matrix-SNH',
|
||||
homePage: 'feed',
|
||||
modules: ['feed', 'blogs', 'opinions', 'chats', 'forum', 'tags', 'trending', 'audios', 'videos',
|
||||
'images', 'bookmarks', 'melody', 'games', 'pixelia', 'favorites']
|
||||
},
|
||||
{
|
||||
key: 'mobile',
|
||||
theme: 'OasisMobile',
|
||||
homePage: 'activity',
|
||||
modules: MOBILE_MODULES
|
||||
}
|
||||
];
|
||||
|
||||
const WORKFLOW_KEYS = WORKFLOWS.map(w => w.key);
|
||||
|
||||
const getWorkflow = (key) => WORKFLOWS.find(w => w.key === String(key || '')) || null;
|
||||
|
||||
const modulesOf = (workflow) => {
|
||||
const set = new Set(workflow.modules.filter(m => ALL_MODULES.includes(m)));
|
||||
return ALL_MODULES.filter(m => set.has(m));
|
||||
};
|
||||
|
||||
const currentWorkflow = (config) => {
|
||||
const modules = (config && config.modules) || {};
|
||||
const theme = (config && config.themes && config.themes.current) || '';
|
||||
return WORKFLOW_KEYS.find(key => {
|
||||
const w = getWorkflow(key);
|
||||
if (theme !== w.theme) return false;
|
||||
const on = new Set(modulesOf(w));
|
||||
return ALL_MODULES.every(m => (modules[`${m}Mod`] === 'on') === on.has(m));
|
||||
}) || null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ALL_MODULES,
|
||||
MOBILE_MODULES,
|
||||
WORKFLOWS,
|
||||
WORKFLOW_KEYS,
|
||||
getWorkflow,
|
||||
modulesOf,
|
||||
currentWorkflow
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
const { div, h2, p, section, button, form, img, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe");
|
||||
const { div, h2, p, section, button, form, img, input, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderContentActions } = require('./main_views');
|
||||
const moment = require('../server/node_modules/moment');
|
||||
const { config } = require('../server/SSB_server.js');
|
||||
|
|
@ -212,7 +212,7 @@ const renderAgendaItem = (item, userId, filter) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.agendaView = async (data, filter) => {
|
||||
exports.agendaView = async (data, filter, q = '') => {
|
||||
const { items = [], counts: _c = {} } = data || {};
|
||||
const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c };
|
||||
return template(
|
||||
|
|
@ -260,6 +260,15 @@ exports.agendaView = async (data, filter) => {
|
|||
`DISCARDED (${counts.discarded})`)
|
||||
)
|
||||
),
|
||||
div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/agenda', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({ type: 'text', name: 'q', value: q, placeholder: i18n.agendaSearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: 'agenda-list' },
|
||||
items.length
|
||||
? items.map(item => renderAgendaItem(item, userId, filter))
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ const {
|
|||
label,
|
||||
option
|
||||
} = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
|
|
@ -47,21 +48,6 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const renderAudioFavoriteToggle = (audioObj, returnTo = "") =>
|
||||
form(
|
||||
{
|
||||
method: "POST",
|
||||
action: audioObj.isFavorite
|
||||
? `/audios/favorites/remove/${encodeURIComponent(audioObj.key)}`
|
||||
: `/audios/favorites/add/${encodeURIComponent(audioObj.key)}`
|
||||
},
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button(
|
||||
{ type: "submit", class: "filter-btn" },
|
||||
audioObj.isFavorite ? i18n.audioRemoveFavoriteButton : i18n.audioAddFavoriteButton
|
||||
)
|
||||
);
|
||||
|
||||
const renderTranscodeButton = (audioObj) =>
|
||||
audioObj.isBcs
|
||||
? form(
|
||||
|
|
@ -108,66 +94,11 @@ const renderAudioOwnerActions = (filter, audioObj, params = {}) => {
|
|||
};
|
||||
|
||||
const renderAudioCommentsSection = (audioId, comments = [], returnTo = null) => {
|
||||
const list = safeArr(comments).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
return renderSharedCommentsSection({
|
||||
action: `/audios/${encodeURIComponent(audioId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
const commentsCount = list.length;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/audios/${encodeURIComponent(audioId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || "";
|
||||
const ts = c?.value?.timestamp || c?.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
const content = c?.value?.content || {};
|
||||
const rootId = content.fork || content.root || null;
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
);
|
||||
};
|
||||
|
||||
const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) => {
|
||||
|
|
@ -177,7 +108,6 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {})
|
|||
? audios.map((audioObj) => {
|
||||
const commentCount = typeof audioObj.commentCount === "number" ? audioObj.commentCount : 0;
|
||||
const title = safeText(audioObj.title);
|
||||
const ownerActions = renderAudioOwnerActions(filter, audioObj, params);
|
||||
|
||||
const isOwn = audioObj.author && String(audioObj.author) === String(userId);
|
||||
return div(
|
||||
|
|
@ -185,55 +115,17 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {})
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`)
|
||||
renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads || null, author: audioObj.author, favKind: 'audios', isFavorite: audioObj.isFavorite, reportTitle: audioObj.title })
|
||||
),
|
||||
div(
|
||||
{ class: "card-section audio-card-body" },
|
||||
div(
|
||||
{ class: "bookmark-topbar" },
|
||||
div(
|
||||
{ class: "bookmark-topbar-left" },
|
||||
renderAudioFavoriteToggle(audioObj, returnTo),
|
||||
audioObj.author && String(audioObj.author) !== String(userId)
|
||||
? form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: audioObj.author }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.audioMessageAuthorButton)
|
||||
)
|
||||
: null
|
||||
),
|
||||
ownerActions.length
|
||||
? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions))
|
||||
: null
|
||||
),
|
||||
title ? h2(title) : null,
|
||||
audioObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(audioObj.lifetime, i18n)) : null,
|
||||
renderAudioPlayer(audioObj),
|
||||
div(
|
||||
{ class: "card-comments-summary" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ":"),
|
||||
span({ class: "card-value" }, String(commentCount)),
|
||||
br(),
|
||||
br(),
|
||||
form(
|
||||
{ method: "GET", action: `/audios/${encodeURIComponent(audioObj.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "filter", value: filter || "all" }),
|
||||
params.q ? input({ type: "hidden", name: "q", value: params.q }) : null,
|
||||
params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton)
|
||||
)
|
||||
renderEngagement(audioObj.key,
|
||||
renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants),
|
||||
renderCommentsLink({ href: `/audios/${encodeURIComponent(audioObj.key)}`, count: commentCount })
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(Object.values(audioObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)))
|
||||
),
|
||||
renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants)
|
||||
),
|
||||
div({ class: "card-spread-left" }, renderSpreadButton(audioObj.key, (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads)),
|
||||
renderMapLocationVisitLabel(audioObj.mapUrl),
|
||||
br(),
|
||||
(() => {
|
||||
|
|
@ -243,12 +135,12 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {})
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(audioObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
|
@ -263,6 +155,7 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => {
|
|||
const returnTo = safeText(params.returnTo) || buildReturnTo("all", params);
|
||||
return div(
|
||||
{ class: "div-center audio-form" },
|
||||
params.spreadWarning || null,
|
||||
form(
|
||||
{
|
||||
action: filter === "edit" ? `/audios/update/${encodeURIComponent(audioId)}` : "/audios/create",
|
||||
|
|
@ -277,7 +170,7 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => {
|
|||
br(),
|
||||
span(i18n.audioTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }),
|
||||
br(),
|
||||
span(i18n.audioDescriptionLabel),
|
||||
br(),
|
||||
|
|
@ -303,20 +196,8 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => {
|
|||
};
|
||||
|
||||
exports.audioView = async (audios, filter = "all", audioId = null, params = {}) => {
|
||||
const title =
|
||||
filter === "mine"
|
||||
? i18n.audioMineSectionTitle
|
||||
: filter === "create"
|
||||
? i18n.audioCreateSectionTitle
|
||||
: filter === "edit"
|
||||
? i18n.audioUpdateSectionTitle
|
||||
: filter === "recent"
|
||||
? i18n.audioRecentSectionTitle
|
||||
: filter === "top"
|
||||
? i18n.audioTopSectionTitle
|
||||
: filter === "favorites"
|
||||
? i18n.audioFavoritesSectionTitle
|
||||
: i18n.audioAllSectionTitle;
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(audioId) };
|
||||
const title = i18n.audioTitle;
|
||||
|
||||
const q = safeText(params.q || "");
|
||||
const sort = safeText(params.sort || "recent");
|
||||
|
|
@ -343,15 +224,15 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {})
|
|||
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop),
|
||||
button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase()),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.audioFilterFavorites
|
||||
String(i18n.audioFilterFavorites).toUpperCase()
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -376,9 +257,9 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {})
|
|||
{ class: "filter-box__controls" },
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.audioSortRecent),
|
||||
option({ value: "oldest", selected: sort === "oldest" }, i18n.audioSortOldest),
|
||||
option({ value: "top", selected: sort === "top" }, i18n.audioSortTop)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.audioSortRecent),
|
||||
option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.audioSortOldest),
|
||||
option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.audioSortTop)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.audioSearchButton)
|
||||
)
|
||||
|
|
@ -407,7 +288,6 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
|||
|
||||
const ownerActions = renderAudioOwnerActions(filter, audioObj, { q, sort });
|
||||
const sideActions = [];
|
||||
sideActions.push(renderAudioFavoriteToggle(audioObj, returnTo));
|
||||
if (audioObj.author && String(audioObj.author) !== String(userId)) {
|
||||
sideActions.push(form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
|
|
@ -425,6 +305,17 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
|||
|
||||
const tagsNode = renderTags(audioObj.tags);
|
||||
|
||||
const detailActions = div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(audioObj.key, null, {
|
||||
author: audioObj.author,
|
||||
favKind: 'audios',
|
||||
isFavorite: audioObj.isFavorite,
|
||||
spread: params.spreads || null,
|
||||
returnTo,
|
||||
reportTitle: audioObj.title
|
||||
})
|
||||
);
|
||||
|
||||
const audioSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
title ? h2({ class: "tribe-card-title" }, title) : null,
|
||||
|
|
@ -435,12 +326,12 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
|||
? p({ class: "tribe-side-description" }, ...renderUrl(audioObj.description))
|
||||
: null,
|
||||
tagsNode,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(audioObj.key, params.spreads)),
|
||||
renderMapLocationVisitLabel(audioObj.mapUrl),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
);
|
||||
|
||||
const audioMain = div({ class: "tribe-main" },
|
||||
detailActions,
|
||||
renderAudioPlayer(audioObj),
|
||||
(() => {
|
||||
const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN;
|
||||
|
|
@ -449,18 +340,20 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(audioObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
})(),
|
||||
renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants),
|
||||
renderAudioCommentsSection(audioObj.key, comments, returnTo)
|
||||
renderEngagement(audioObj.key,
|
||||
renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants),
|
||||
renderAudioCommentsSection(audioObj.key, comments, returnTo)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
|
|
@ -476,15 +369,15 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
|||
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop),
|
||||
button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase()),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.audioFilterFavorites
|
||||
String(i18n.audioFilterFavorites).toUpperCase()
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
@ -499,7 +392,7 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload
|
|||
const title = i18n.audioTranscodeDetailTitle || "Transcode";
|
||||
const composition = Array.isArray(audio.bcsComposition) ? audio.bcsComposition : [];
|
||||
const hasStego = decoded && stegoPayload && (stegoPayload.id || stegoPayload.ts || stegoPayload.msg);
|
||||
const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm:ss") : null;
|
||||
const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm") : null;
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -520,7 +413,7 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload
|
|||
p({ class: "transcode-meta card-footer" },
|
||||
userLink(audio.author),
|
||||
span({ class: "melody-meta-sep" }, " · "),
|
||||
span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm:ss")),
|
||||
span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
itemSize ? span({ class: "melody-meta-sep" }, " · ") : null,
|
||||
itemSize ? renderEcoTax(itemSize, audio.key) : null
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const FILTER_LABELS = {
|
|||
feed: i18n.typeFeed, event: i18n.typeEvent, task: i18n.typeTask, report: i18n.typeReport,
|
||||
image: i18n.typeImage, audio: i18n.typeAudio, video: i18n.typeVideo, post: i18n.typePost,
|
||||
forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub,
|
||||
transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe,
|
||||
transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, housing: i18n.typeHousing, tribe: i18n.typeTribe,
|
||||
project: i18n.typeProject, industry: i18n.typeIndustry, industryBlueprint: i18n.industryBlueprints, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim,
|
||||
aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts,
|
||||
map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product',
|
||||
|
|
@ -20,7 +20,7 @@ const FILTER_LABELS = {
|
|||
const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs'];
|
||||
const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts'];
|
||||
const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
|
||||
const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'industry', 'industryBlueprint', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore'];
|
||||
const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore'];
|
||||
const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent'];
|
||||
|
||||
const SEARCH_FIELDS = ['author','id','from','to'];
|
||||
|
|
@ -100,7 +100,7 @@ const computeStats = (blocks) => {
|
|||
|
||||
const renderStatsPanel = (stats, currentFilter, search) => {
|
||||
if (!stats || stats.total === 0) return null;
|
||||
const topTypes = stats.typeBreakdown.slice(0, 10);
|
||||
const topTypes = stats.typeBreakdown.filter(t => t.type !== 'industryBlueprint').slice(0, 10);
|
||||
return div({ class: 'blockchain-stats' },
|
||||
div({ class: 'tags-header' },
|
||||
h3(`${i18n.blockchainStatsTitle || 'Stats'} (${stats.total})`)
|
||||
|
|
@ -179,6 +179,7 @@ const getViewDetailsAction = (type, block) => {
|
|||
case 'pub': return `/invites`;
|
||||
case 'market': return `/market/${encodeURIComponent(block.id)}`;
|
||||
case 'job': return `/jobs/${encodeURIComponent(block.id)}`;
|
||||
case 'housing': return `/housing/${encodeURIComponent(block.id)}`;
|
||||
case 'project': return `/projects/${encodeURIComponent(block.id)}`;
|
||||
case 'industry': return `/industry/${encodeURIComponent(block.id)}`;
|
||||
case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(block.id)}`;
|
||||
|
|
@ -209,6 +210,7 @@ const getViewDetailsAction = (type, block) => {
|
|||
case 'chat': return `/chats/${encodeURIComponent(block.id)}`;
|
||||
case 'gameScore': return `/games?filter=scoring`;
|
||||
case 'log': return `/logs/view/${encodeURIComponent(block.id)}`;
|
||||
case 'poll': return `/polls/${encodeURIComponent(block.id)}`;
|
||||
case 'calendarDate':
|
||||
case 'calendarNote': return block.content?.calendarId ? `/calendars/${encodeURIComponent(block.content.calendarId)}` : `/calendars`;
|
||||
case 'padEntry': return block.content?.padId ? `/pads/${encodeURIComponent(block.content.padId)}` : `/pads`;
|
||||
|
|
@ -221,7 +223,7 @@ const TYPE_COLORS = {
|
|||
post:'#3498db', vote:'#9b59b6', votes:'#9b59b6', about:'#1abc9c', contact:'#16a085',
|
||||
pub:'#2ecc71', tribe:'#e67e22', event:'#e74c3c', task:'#f39c12', report:'#c0392b',
|
||||
image:'#2980b9', audio:'#8e44ad', video:'#d35400', document:'#27ae60', bookmark:'#f1c40f',
|
||||
forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db',
|
||||
forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', housing:'#16a085',
|
||||
project:'#2ecc71', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12',
|
||||
pixelia:'#9b59b6', curriculum:'#1abc9c', aiExchange:'#3498db', tombstone:'#7f8c8d',
|
||||
parliamentTerm:'#8e44ad', parliamentProposal:'#8e44ad', parliamentLaw:'#8e44ad',
|
||||
|
|
@ -416,9 +418,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi
|
|||
button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
|
||||
),
|
||||
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
||||
form({ method:'GET', action:getViewDetailsAction(block.type, block) },
|
||||
button({ type:'submit', class:'filter-btn' }, i18n.visitContent)
|
||||
)
|
||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
||||
: (block.isTombstoned || block.isReplaced) ?
|
||||
div({ class: 'deleted-label' },
|
||||
i18n.blockchainContentDeleted || "This content has been deleted."
|
||||
|
|
@ -474,7 +474,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {})
|
|||
input({ type: 'datetime-local', name: 'to', value: toVal, class: 'blockexplorer-search-input' })
|
||||
),
|
||||
div({ class: 'blockexplorer-search-actions' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit)
|
||||
div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -526,11 +526,9 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {})
|
|||
div({ class:'block' },
|
||||
div({ class:'block-buttons' },
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'),
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram || 'Datagram' }, '⊞'),
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, '⊞'),
|
||||
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
||||
form({ method:'GET', action:getViewDetailsAction(block.type, block) },
|
||||
button({ type:'submit', class:'filter-btn' }, i18n.visitContent)
|
||||
)
|
||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
||||
: (block.isTombstoned || block.isReplaced) ?
|
||||
div({ class: 'deleted-label' },
|
||||
i18n.blockchainContentDeleted || "This content has been deleted."
|
||||
|
|
|
|||
179
src/views/blog_view.js
Normal file
179
src/views/blog_view.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
const { renderTextWithStyles } = require("../backend/renderTextWithStyles");
|
||||
const { sanitizeHtml } = require("../backend/sanitizeHtml");
|
||||
|
||||
const userId = config.keys.id;
|
||||
|
||||
const FILTERS = [
|
||||
{ key: "ALL", i18n: "blogFilterAll" },
|
||||
{ key: "MINE", i18n: "blogFilterMine" },
|
||||
{ key: "RECENT", i18n: "blogFilterRecent" },
|
||||
{ key: "FAVORITES", i18n: "blogFilterFavorites" },
|
||||
{ key: "TOP", i18n: "blogFilterTop" }
|
||||
];
|
||||
|
||||
const safeText = (v) => String(v == null ? "" : v).trim();
|
||||
|
||||
const excerpt = (text, max = 420) => {
|
||||
const s = safeText(text);
|
||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
||||
};
|
||||
|
||||
const renderFilterBar = (filter, q, showSearch = true) =>
|
||||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/blogs", class: "ui-toolbar ui-toolbar--filters" },
|
||||
...FILTERS.map(f =>
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase())
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.blogCreateButton)
|
||||
)
|
||||
),
|
||||
showSearch
|
||||
? div({ class: "filters" },
|
||||
form({ method: "GET", action: "/blogs", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q || "", placeholder: i18n.blogSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const renderBlogCard = (blog, filter, spreadInfo) => {
|
||||
const href = `/blogs/${encodeURIComponent(blog.id)}`;
|
||||
const isOwn = String(blog.author) === String(userId);
|
||||
return div({ class: "trending-card blog-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(blog.id, href, { spread: spreadInfo || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text })
|
||||
),
|
||||
div({ class: "card-section blog-card-body" },
|
||||
blog.subject
|
||||
? div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, a({ href }, blog.subject))
|
||||
)
|
||||
: null,
|
||||
div({ class: "blog-card-text", innerHTML: sanitizeHtml(renderTextWithStyles(excerpt(blog.text))) }),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
span(" · "),
|
||||
userLink(blog.author)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderCreateForm = () =>
|
||||
section(
|
||||
div({ class: "publish-form" },
|
||||
form({ action: "/blogs/create", method: "POST", enctype: "multipart/form-data" },
|
||||
label({ for: "subject" }, i18n.blogSubject),
|
||||
br(),
|
||||
input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder }),
|
||||
br(),
|
||||
br(),
|
||||
label({ for: "text" }, i18n.blogMessage),
|
||||
br(),
|
||||
textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: "7000", placeholder: i18n.publishWarningPlaceholder }),
|
||||
br(),
|
||||
br(),
|
||||
label({ for: "blob" }, i18n.blogMedia),
|
||||
br(),
|
||||
input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }),
|
||||
br(),
|
||||
br(),
|
||||
div({ class: "blog-allow-comments" },
|
||||
input({ type: "hidden", name: "allowComments", value: "0" }),
|
||||
label(
|
||||
input({ type: "checkbox", name: "allowComments", value: "1", checked: true }),
|
||||
" ",
|
||||
i18n.blogAllowComments
|
||||
)
|
||||
),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.blogPublish)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const renderCommentsSection = (blog, comments = []) => {
|
||||
const href = `/blogs/${encodeURIComponent(blog.id)}`;
|
||||
return renderSharedCommentsSection({
|
||||
action: `${href}/comments`,
|
||||
comments: comments,
|
||||
returnTo: href,
|
||||
closedNote: blog.allowComments ? null : i18n.blogCommentsClosed
|
||||
});
|
||||
};
|
||||
|
||||
exports.blogView = async (blogs = [], filter = "ALL", params = {}) => {
|
||||
const showForm = String(filter).toUpperCase() === "CREATE";
|
||||
const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map();
|
||||
return template(
|
||||
i18n.blogTitle,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.blogTitle),
|
||||
p(i18n.blogDescription)
|
||||
)
|
||||
),
|
||||
renderFilterBar(showForm ? "ALL" : filter, params.q, !showForm),
|
||||
showForm
|
||||
? renderCreateForm()
|
||||
: section(
|
||||
blogs.length
|
||||
? div({ class: "jobs-grid" }, ...blogs.map(b => renderBlogCard(b, filter, spreadMap.get(b.id))))
|
||||
: p({ class: "no-content" }, i18n.blogNoItems)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.singleBlogView = async (blog, comments = [], params = {}) => {
|
||||
const href = `/blogs/${encodeURIComponent(blog.id)}`;
|
||||
const isAuthor = String(blog.author) === String(userId);
|
||||
|
||||
const blogSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(blog.id, null, { spread: params.spreads || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, blog.subject || i18n.blogTitle)
|
||||
),
|
||||
table({ class: "tribe-info-table jobs-info-table" },
|
||||
tr(
|
||||
td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm"))
|
||||
),
|
||||
tr(
|
||||
td({ class: "tribe-info-value", colspan: "4" }, userLink(blog.author))
|
||||
)
|
||||
),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.blogComments}: ${blog.commentCount || 0}`)
|
||||
),
|
||||
);
|
||||
|
||||
const blogMain = div({ class: "tribe-main" },
|
||||
div({ class: "job-section" },
|
||||
div({ class: "blog-detail-text", innerHTML: sanitizeHtml(renderTextWithStyles(blog.text || "")) })
|
||||
),
|
||||
renderEngagement(blog.id,
|
||||
renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants),
|
||||
renderCommentsSection(blog, comments)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
blog.subject || i18n.blogTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.blogTitle), p(i18n.blogDescription))),
|
||||
renderFilterBar("ALL", params.q, false),
|
||||
section(div({ class: "tribe-details" }, blogSide, blogMain))
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views")
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderSpreadEditWarning, renderContentActions, renderDocumentActions } = require("./main_views")
|
||||
const { renderMapLocationVisitLabel } = require("./maps_view")
|
||||
const { renderEncryptedChip } = require("./clearnet_view")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
|
|
@ -20,13 +21,6 @@ const renderNoteText = (text) => {
|
|||
return result
|
||||
}
|
||||
|
||||
const renderCalendarFavoriteToggle = (cal, returnTo) =>
|
||||
form(
|
||||
{ method: "POST", action: cal.isFavorite ? `/calendars/favorites/remove/${encodeURIComponent(cal.rootId)}` : `/calendars/favorites/add/${encodeURIComponent(cal.rootId)}` },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button({ type: "submit", class: "tribe-action-btn" }, cal.isFavorite ? (i18n.calendarRemoveFavorite || "Remove Favorite") : (i18n.calendarAddFavorite || "Add Favorite"))
|
||||
)
|
||||
|
||||
const renderModeButtons = (currentFilter) =>
|
||||
div({ class: "tribe-mode-buttons" },
|
||||
["all", "mine", "recent", "favorites", "open", "closed"].map(f =>
|
||||
|
|
@ -63,62 +57,62 @@ const renderCalendarCard = (cal, spreadInfo) => {
|
|||
renderLifespanChip(cal.lifetime, i18n)
|
||||
].filter(Boolean)
|
||||
return div({ class: "tribe-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(cal.rootId, href, { spread: spreadInfo || null, author: cal.author, favKind: 'calendars', isFavorite: cal.isFavorite, reportTitle: cal.title })
|
||||
),
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, a({ href }, cal.title || "\u2014"))
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
cal.deadline
|
||||
? p({ class: "job-meta-line" }, `${i18n.calendarDeadlineLabel || "Deadline"}: ${moment(cal.deadline).format("YYYY-MM-DD HH:mm")}`)
|
||||
? p({ class: "job-meta-line" }, `${i18n.calendarDeadlineLabel || "Deadline"}: ${moment(cal.deadline).format("YYYY/MM/DD HH:mm")}`)
|
||||
: null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(cal.rootId, spreadInfo)),
|
||||
div({ class: "card-visit-btn-centered" },
|
||||
a({ href, class: "filter-btn" }, i18n.calendarVisitCalendar || "Visit Calendar")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const renderIntervalBlock = () =>
|
||||
div({ class: "calendar-interval-block" },
|
||||
const renderIntervalBlock = (bounds = {}, current = "", until = "") => {
|
||||
const sel = (v) => (String(current || "") === v ? { selected: true } : {})
|
||||
return div({ class: "calendar-interval-block" },
|
||||
span({ class: "calendar-interval-label" }, i18n.calendarIntervalLabel || "Interval"),
|
||||
div({ class: "calendar-interval-row" },
|
||||
input({ type: "hidden", name: "intervalWeekly", value: "0" }),
|
||||
label({ class: "calendar-interval-option" },
|
||||
input({ type: "checkbox", name: "intervalWeekly", value: "1" }),
|
||||
" ", i18n.calendarIntervalWeekly || "Weekly"
|
||||
),
|
||||
input({ type: "hidden", name: "intervalMonthly", value: "0" }),
|
||||
label({ class: "calendar-interval-option" },
|
||||
input({ type: "checkbox", name: "intervalMonthly", value: "1" }),
|
||||
" ", i18n.calendarIntervalMonthly || "Monthly"
|
||||
),
|
||||
input({ type: "hidden", name: "intervalYearly", value: "0" }),
|
||||
label({ class: "calendar-interval-option" },
|
||||
input({ type: "checkbox", name: "intervalYearly", value: "1" }),
|
||||
" ", i18n.calendarIntervalYearly || "Yearly"
|
||||
)
|
||||
select({ name: "interval", class: "calendar-interval-select" },
|
||||
option({ value: "", ...sel("") }, i18n.calendarIntervalNone),
|
||||
option({ value: "weekly", ...sel("weekly") }, i18n.calendarIntervalWeekly || "Weekly"),
|
||||
option({ value: "monthly", ...sel("monthly") }, i18n.calendarIntervalMonthly || "Monthly"),
|
||||
option({ value: "yearly", ...sel("yearly") }, i18n.calendarIntervalYearly || "Yearly")
|
||||
),
|
||||
span({ class: "calendar-interval-label calendar-interval-until" }, i18n.calendarIntervalUntil || "Until"),
|
||||
input({ type: "datetime-local", name: "intervalDeadline" }),
|
||||
input({
|
||||
type: "datetime-local",
|
||||
name: "intervalDeadline",
|
||||
...(until ? { value: until } : {}),
|
||||
...(bounds.min ? { min: bounds.min } : {}),
|
||||
...(bounds.max ? { max: bounds.max } : {})
|
||||
}),
|
||||
br()
|
||||
)
|
||||
}
|
||||
|
||||
const renderCreateForm = (calendarToEdit, params) => {
|
||||
const isEdit = !!calendarToEdit
|
||||
const tribeId = (params && params.tribeId) || ""
|
||||
const now = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm")
|
||||
const deadlineMax = calendarToEdit && calendarToEdit.deadline
|
||||
? moment(calendarToEdit.deadline).format("YYYY-MM-DDTHH:mm")
|
||||
: ""
|
||||
const action = isEdit ? `/calendars/update/${encodeURIComponent(calendarToEdit.rootId)}` : "/calendars/create"
|
||||
const sectionTitle = isEdit ? (i18n.calendarUpdateSectionTitle || "Update Calendar") : (i18n.calendarCreateSectionTitle || "Create New Calendar")
|
||||
return div({ class: "div-center audio-form" },
|
||||
h2(sectionTitle),
|
||||
(params && params.spreadWarning) || null,
|
||||
form({ method: "POST", action },
|
||||
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
|
||||
span(i18n.calendarTitleLabel || "Title"), br(),
|
||||
input({ type: "text", name: "title", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: calendarToEdit ? calendarToEdit.title : "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: calendarToEdit ? calendarToEdit.title : "" }),
|
||||
br(), br(),
|
||||
span(i18n.calendarStatusLabel || "Status"), br(),
|
||||
select({ name: "status", required: true },
|
||||
|
|
@ -132,17 +126,21 @@ const renderCreateForm = (calendarToEdit, params) => {
|
|||
span(i18n.calendarTagsLabel || "Tags"), br(),
|
||||
input({ type: "text", name: "tags", placeholder: i18n.calendarTagsPlaceholder || "tag1, tag2...", value: calendarToEdit && Array.isArray(calendarToEdit.tags) ? calendarToEdit.tags.join(", ") : "" }),
|
||||
br(), br(),
|
||||
span(i18n.mapLocationTitle || "Map Location"), br(),
|
||||
input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: (calendarToEdit && calendarToEdit.mapUrl) || "" }),
|
||||
br(), br(),
|
||||
!isEdit
|
||||
? [
|
||||
span(i18n.calendarFirstDateLabel || "Date"), br(),
|
||||
input({ type: "datetime-local", name: "firstDate", required: true, min: now }),
|
||||
input({ type: "datetime-local", name: "firstDate", required: true, min: now, max: deadlineMax || undefined }),
|
||||
br(), br(),
|
||||
span(i18n.calendarFormDescription || "Description"), br(),
|
||||
input({ type: "text", name: "firstDateLabel", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }),
|
||||
br(), br(),
|
||||
renderIntervalBlock(),
|
||||
span(i18n.calendarFirstNoteLabel || "Notes"), br(),
|
||||
textarea({ name: "firstNote", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }),
|
||||
br(), br(),
|
||||
renderIntervalBlock({ min: now, max: deadlineMax || undefined }),
|
||||
br(), br()
|
||||
]
|
||||
: null,
|
||||
|
|
@ -192,17 +190,10 @@ exports.renderCalendarInvitePage = (code) => {
|
|||
}
|
||||
|
||||
exports.calendarsView = async (calendars, filter, calendarToEdit, params) => {
|
||||
if (calendarToEdit) params = { ...(params || {}), spreadWarning: await renderSpreadEditWarning(calendarToEdit.id || calendarToEdit.key || calendarToEdit.rootId) };
|
||||
const q = (params && params.q) || ""
|
||||
const showForm = filter === "create" || filter === "edit" || !!calendarToEdit
|
||||
const headerMap = {
|
||||
all: i18n.calendarAllSectionTitle || "Calendars",
|
||||
mine: i18n.calendarMineSectionTitle || "Your Calendars",
|
||||
recent: i18n.calendarRecentSectionTitle || "Recent Calendars",
|
||||
favorites: i18n.calendarFavoritesSectionTitle || "Favorites",
|
||||
open: i18n.calendarOpenSectionTitle || "Open Calendars",
|
||||
closed: i18n.calendarClosedSectionTitle || "Closed Calendars"
|
||||
}
|
||||
const headerText = headerMap[filter] || headerMap.all
|
||||
const headerText = i18n.calendarsTitle || "Calendars"
|
||||
|
||||
return template(
|
||||
i18n.calendarsTitle || "Calendars",
|
||||
|
|
@ -212,14 +203,17 @@ exports.calendarsView = async (calendars, filter, calendarToEdit, params) => {
|
|||
p(i18n.calendarsDescription || "Discover and manage calendars.")
|
||||
),
|
||||
renderModeButtons(filter),
|
||||
q
|
||||
? div({ class: "filters" },
|
||||
form({ method: "GET", action: "/calendars" },
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars..." }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.searchButton || "Search")
|
||||
showForm
|
||||
? null
|
||||
: div({ class: "filters" },
|
||||
form({ method: "GET", action: "/calendars", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars...", class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null
|
||||
),
|
||||
section(
|
||||
showForm
|
||||
|
|
@ -266,26 +260,17 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => {
|
|||
h2({ class: "tribe-card-title" }, calendar.title || "\u2014")
|
||||
),
|
||||
detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(calendar.rootId, params && params.spreads)),
|
||||
div({ class: "shop-share" },
|
||||
span({ class: "tribe-info-label" }, i18n.calendarsShareUrl || "Share URL"),
|
||||
input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" })
|
||||
),
|
||||
table({ class: "tribe-info-table" },
|
||||
tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY-MM-DD"))),
|
||||
tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY/MM/DD HH:mm"))),
|
||||
calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY/MM/DD HH:mm"))) : null,
|
||||
tr(td({ class: "tribe-info-label" }, i18n.calendarStatusLabel || "Status"), td({ class: "tribe-info-value", colspan: "3" }, renderStatus(calendar))),
|
||||
tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))),
|
||||
calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY-MM-DD HH:mm"))) : null
|
||||
tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author)))
|
||||
),
|
||||
div({ class: "tribe-side-actions" },
|
||||
renderCalendarFavoriteToggle(calendar, shareUrl),
|
||||
isAuthor
|
||||
? form({ method: "GET", action: "/calendars" },
|
||||
input({ type: "hidden", name: "filter", value: "edit" }),
|
||||
input({ type: "hidden", name: "id", value: calendar.rootId }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update")
|
||||
)
|
||||
: null,
|
||||
tags,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`)
|
||||
),
|
||||
div({ class: "tribe-side-actions calendar-invite-actions" },
|
||||
isAuthor && calendar.status !== "OPEN"
|
||||
? form({ method: "POST", action: `/calendars/generate-invite/${encodeURIComponent(calendar.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite)
|
||||
|
|
@ -295,27 +280,18 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => {
|
|||
if (!(isAuthor && !calendar.tribeId)) return null
|
||||
const invs = Array.isArray(calendar.invites) ? calendar.invites : []
|
||||
const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code)
|
||||
if (openInvite) return [
|
||||
if (openInvite) return div({ class: "calendar-open-invite-block" },
|
||||
div({ class: "tribe-open-invite" },
|
||||
span({ class: "card-label" }, i18n.tribeInviteCodeText),
|
||||
span({ class: "tribe-open-invite-code" }, openInvite.code)
|
||||
),
|
||||
form({ method: "POST", action: `/calendars/open-invite/remove/${encodeURIComponent(calendar.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation)
|
||||
)
|
||||
]
|
||||
)
|
||||
return form({ method: "POST", action: `/calendars/open-invite/create/${encodeURIComponent(calendar.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation)
|
||||
)
|
||||
})(),
|
||||
isAuthor
|
||||
? form({ method: "POST", action: `/calendars/delete/${encodeURIComponent(calendar.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.calendarDelete || "Delete")
|
||||
)
|
||||
: null,
|
||||
!isAuthor
|
||||
? a({ href: `/pm?to=${encodeURIComponent(calendar.author)}`, class: "tribe-action-btn" }, "PM")
|
||||
: null,
|
||||
!isAuthor && !isParticipant && calendar.status === "OPEN"
|
||||
? form({ method: "POST", action: `/calendars/join/${encodeURIComponent(calendar.rootId)}` },
|
||||
button({ type: "submit", class: "create-button" }, i18n.calendarJoin || "Join Calendar")
|
||||
|
|
@ -330,34 +306,43 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => {
|
|||
)
|
||||
: null
|
||||
),
|
||||
tags,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`)
|
||||
)
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions calendar-owner-actions" },
|
||||
form({ method: "GET", action: "/calendars" },
|
||||
input({ type: "hidden", name: "filter", value: "edit" }),
|
||||
input({ type: "hidden", name: "id", value: calendar.rootId }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update")
|
||||
),
|
||||
form({ method: "POST", action: `/calendars/delete/${encodeURIComponent(calendar.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.calendarDelete || "Delete")
|
||||
)
|
||||
)
|
||||
: null,
|
||||
renderDocumentActions('calendars', calendar.rootId)
|
||||
)
|
||||
|
||||
const minDate = now.add(1, "minute").format("YYYY-MM-DDTHH:mm")
|
||||
const calMax = calendar.deadline ? moment(calendar.deadline).format("YYYY-MM-DDTHH:mm") : ""
|
||||
const canAddDate = !calClosed && (calendar.status === "OPEN" || isAuthor)
|
||||
|
||||
const unifiedForm = canAddDate
|
||||
? div({ class: "div-center audio-form" },
|
||||
h4(i18n.calendarAddEntry || "Add Entry"),
|
||||
form({ method: "POST", action: `/calendars/add-date/${encodeURIComponent(calendar.rootId)}` },
|
||||
span(i18n.calendarDateLabel || "Date"), br(),
|
||||
input({ type: "datetime-local", name: "date", required: true, min: minDate }),
|
||||
input({ type: "datetime-local", name: "date", required: true, min: minDate, max: calMax || undefined }),
|
||||
br(), br(),
|
||||
span(i18n.calendarFormDescription || "Description"), br(),
|
||||
input({ type: "text", name: "label", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }),
|
||||
br(), br(),
|
||||
renderIntervalBlock(),
|
||||
br(),
|
||||
isParticipant
|
||||
? [
|
||||
span(i18n.calendarNoteLabel || "Note (optional)"), br(),
|
||||
span(i18n.calendarNoteLabel), br(),
|
||||
textarea({ name: "text", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }),
|
||||
br(), br()
|
||||
]
|
||||
: null,
|
||||
renderIntervalBlock({ min: minDate, max: calMax || undefined }),
|
||||
br(),
|
||||
button({ type: "submit", class: "create-button" }, i18n.calendarAddEntry || "Add Entry")
|
||||
)
|
||||
)
|
||||
|
|
@ -391,7 +376,7 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => {
|
|||
)
|
||||
: null,
|
||||
div({ class: "calendar-date-item-header" },
|
||||
`${moment(d.date).format("YYYY-MM-DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}`
|
||||
`${moment(d.date).format("YYYY/MM/DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}`
|
||||
),
|
||||
(() => {
|
||||
const visibleNotes = notes.filter(n => n.text && String(n.text).trim())
|
||||
|
|
@ -424,6 +409,17 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => {
|
|||
: null
|
||||
|
||||
const calMain = div({ class: "tribe-main" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(calendar.rootId, null, {
|
||||
author: calendar.author,
|
||||
favKind: 'calendars',
|
||||
isFavorite: calendar.isFavorite,
|
||||
spread: (params && params.spreads) || null,
|
||||
returnTo: shareUrl,
|
||||
reportTitle: calendar.title
|
||||
})
|
||||
),
|
||||
|
||||
calNav,
|
||||
grid,
|
||||
dayNotesSection,
|
||||
|
|
@ -442,3 +438,5 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => {
|
|||
section(div({ class: "tribe-details" }, calSide, calMain))
|
||||
)
|
||||
}
|
||||
|
||||
exports.renderIntervalBlock = renderIntervalBlock
|
||||
|
|
|
|||
|
|
@ -1069,17 +1069,18 @@ const CaseSearch = (filter, search = '') =>
|
|||
div(
|
||||
{ class: 'filters' },
|
||||
form(
|
||||
{ method: 'GET', action: '/courts' },
|
||||
{ method: 'GET', action: '/courts', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({
|
||||
type: 'text',
|
||||
name: 'search',
|
||||
placeholder: i18n.searchCasesPlaceholder,
|
||||
value: search || ''
|
||||
value: search || '',
|
||||
class: 'filter-box__input'
|
||||
}),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.applyFilters),
|
||||
br()
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderVisibilityChip } = require('./main_views');
|
||||
const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span, table, tr, td } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderVisibilityChip, renderDocumentActions } = require('./main_views');
|
||||
const { renderUrl } = require('../backend/renderUrl');
|
||||
|
||||
const generateCVBox = (label, content, className) => {
|
||||
|
|
@ -9,22 +9,8 @@ const generateCVBox = (label, content, className) => {
|
|||
);
|
||||
};
|
||||
|
||||
const generateTags = (tags) => {
|
||||
return tags && 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;
|
||||
};
|
||||
|
||||
exports.createCVView = async (cv = {}, editMode = false) => {
|
||||
const title = editMode ? i18n.cvEditSectionTitle : i18n.cvCreateSectionTitle;
|
||||
const title = i18n.cvTitle;
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -40,6 +26,20 @@ exports.createCVView = async (cv = {}, editMode = false) => {
|
|||
enctype: "multipart/form-data"
|
||||
},
|
||||
|
||||
div({ class: "cv-box cv-ai-box" },
|
||||
div({ class: "poll-switch" },
|
||||
input({ type: "hidden", name: "aiManaged", value: "0" }),
|
||||
label(
|
||||
input({ type: "checkbox", name: "aiManaged", value: "1", ...(cv.aiManaged === false ? {} : { checked: true }) }),
|
||||
" ", i18n.cvAiManaged
|
||||
)
|
||||
),
|
||||
div({ class: "cv-threshold-row" },
|
||||
label({ for: "cv_match_threshold" }, `${i18n.cvMatchThreshold}: `),
|
||||
input({ type: "number", id: "cv_match_threshold", name: "matchThreshold", min: "0", max: "100", step: "5", value: String(cv.matchThreshold != null ? cv.matchThreshold : 80) })
|
||||
)
|
||||
),
|
||||
|
||||
generateCVBox(i18n.cvPersonal, [
|
||||
label(i18n.cvNameLabel), br(),
|
||||
input({ type: "text", name: "name", required: true, value: cv.name || "" }), br(),
|
||||
|
|
@ -125,12 +125,99 @@ exports.cvView = async (cv) => {
|
|||
)
|
||||
}
|
||||
|
||||
const hasPersonal = cv.contact || cv.name || cv.description || cv.photo || typeof cv.oasisContributor === "boolean" || (cv.personalSkills && cv.personalSkills.length);
|
||||
const hasPersonalExp = cv.personalExperiences;
|
||||
const hasOasis = cv.oasisExperiences || (cv.oasisSkills && cv.oasisSkills.length);
|
||||
const hasEducational = cv.educationExperiences || cv.languages || (cv.educationalSkills && cv.educationalSkills.length);
|
||||
const hasProfessional = cv.professionalExperiences || (cv.professionalSkills && cv.professionalSkills.length);
|
||||
const hasAvailability = cv.location || cv.status || cv.preferences;
|
||||
const skills = (list) => (Array.isArray(list) ? list.filter(Boolean) : []);
|
||||
const allSkills = [
|
||||
...skills(cv.personalSkills),
|
||||
...skills(cv.educationalSkills),
|
||||
...skills(cv.professionalSkills),
|
||||
...skills(cv.oasisSkills)
|
||||
];
|
||||
const uniqueSkills = Array.from(new Set(allSkills));
|
||||
|
||||
const renderSkillTags = (list) => skills(list).length
|
||||
? div({ class: "card-tags" },
|
||||
skills(list).map(tag =>
|
||||
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))
|
||||
)
|
||||
: null;
|
||||
|
||||
const renderBlock = (heading, body, list) => (body || skills(list).length)
|
||||
? div({ class: "cv-box" },
|
||||
h2({ class: "job-section-title" }, heading),
|
||||
body ? p({ class: "tribe-side-description" }, ...renderUrl(String(body))) : null,
|
||||
renderSkillTags(list)
|
||||
)
|
||||
: null;
|
||||
|
||||
const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC';
|
||||
const nextVis = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC';
|
||||
|
||||
const infoRows = [];
|
||||
const pushRow = (labelText, valueNode) =>
|
||||
infoRows.push(tr(
|
||||
td({ class: "tribe-info-label" }, labelText),
|
||||
td({ class: "tribe-info-value" }, valueNode)
|
||||
));
|
||||
if (cv.location) pushRow(i18n.cvLocationLabel, cv.location);
|
||||
if (cv.status) pushRow(i18n.cvStatusLabel, cv.status);
|
||||
if (cv.preferences) pushRow(i18n.cvPreferencesLabel, cv.preferences);
|
||||
if (cv.languages) pushRow(i18n.cvLanguagesLabel, String(cv.languages).toUpperCase());
|
||||
pushRow(i18n.cvAiManaged, cv.aiManaged === false ? i18n.switchOff : `${i18n.switchOn} · ${cv.matchThreshold != null ? cv.matchThreshold : 80}%`);
|
||||
pushRow(i18n.cvCreatedAt, new Date(cv.createdAt).toLocaleString());
|
||||
if (cv.updatedAt) pushRow(i18n.cvUpdatedAt, new Date(cv.updatedAt).toLocaleString());
|
||||
|
||||
const cvSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, cv.name || i18n.unnamed || 'Anonymous')
|
||||
),
|
||||
cv.photo
|
||||
? img({ src: `/blob/${encodeURIComponent(cv.photo)}`, class: "cv-photo tribe-detail-image" })
|
||||
: null,
|
||||
(cv.contact || cv.author)
|
||||
? div({ class: 'profile-qr' },
|
||||
a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}` },
|
||||
img({ class: 'profile-qr-img', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=240`, alt: 'QR' })))
|
||||
: null,
|
||||
cv.contact ? p(userLink(cv.contact)) : null,
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
uniqueSkills.length
|
||||
? div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.cvSkillsCount}: ${uniqueSkills.length}`)
|
||||
)
|
||||
: null,
|
||||
div({ class: "tribe-side-actions cv-visibility-row housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `),
|
||||
renderVisibilityChip(vis, i18n),
|
||||
form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" },
|
||||
input({ type: "hidden", name: "visibility", value: nextVis }),
|
||||
button({ type: "submit", class: "filter-btn" },
|
||||
nextVis === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden')
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: "tribe-side-actions" },
|
||||
form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` },
|
||||
button({ type: "submit", class: "update-btn" }, i18n.cvEditButton)
|
||||
),
|
||||
form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` },
|
||||
button({ type: "submit", class: "delete-btn" }, i18n.cvDeleteButton)
|
||||
)
|
||||
),
|
||||
renderDocumentActions('cv', null)
|
||||
);
|
||||
|
||||
const cvMain = div({ class: "tribe-main" },
|
||||
cv.description
|
||||
? div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, i18n.cvDescriptionLabel),
|
||||
p({ class: "tribe-side-description" }, ...renderUrl(String(cv.description)))
|
||||
)
|
||||
: null,
|
||||
renderBlock(i18n.cvPersonal, cv.personalExperiences, cv.personalSkills),
|
||||
renderBlock(i18n.cvEducationalView, cv.educationExperiences, cv.educationalSkills),
|
||||
renderBlock(i18n.cvProfessionalView, cv.professionalExperiences, cv.professionalSkills),
|
||||
renderBlock(i18n.cvOasisContributorView, cv.oasisExperiences, cv.oasisSkills)
|
||||
);
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -139,119 +226,7 @@ exports.cvView = async (cv) => {
|
|||
h2(title),
|
||||
p(i18n.cvDescription)
|
||||
),
|
||||
div({ class: "cv-section" },
|
||||
div({ class: "cv-item" }, ...[
|
||||
(() => {
|
||||
const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC';
|
||||
const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC';
|
||||
return div({ class: "cv-visibility-row" },
|
||||
span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `),
|
||||
renderVisibilityChip(vis, i18n),
|
||||
" ",
|
||||
form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" },
|
||||
input({ type: "hidden", name: "visibility", value: next }),
|
||||
button({ type: "submit", class: "filter-btn" },
|
||||
next === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden')
|
||||
)
|
||||
)
|
||||
);
|
||||
})(),
|
||||
div({ class: "cv-actions" },
|
||||
form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` },
|
||||
button({ type: "submit" }, i18n.cvEditButton)
|
||||
),
|
||||
form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` },
|
||||
button({ type: "submit", class: "danger-btn" }, i18n.cvDeleteButton)
|
||||
)
|
||||
),
|
||||
div({ class: "cv-meta" },
|
||||
p(`${i18n.cvCreatedAt}: ${new Date(cv.createdAt).toLocaleString()}`),
|
||||
cv.updatedAt ? p(`${i18n.cvUpdatedAt}: ${new Date(cv.updatedAt).toLocaleString()}`) : null
|
||||
),
|
||||
hasPersonal ? div({ class: "cv-box personal" }, ...[
|
||||
cv.photo
|
||||
? img({
|
||||
src: `/blob/${encodeURIComponent(cv.photo)}`,
|
||||
class: "cv-photo"
|
||||
})
|
||||
: null,
|
||||
cv.name ? h2(`${cv.name}`) : null,
|
||||
(cv.contact || cv.author)
|
||||
? a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}`, class: 'inhabitant-qr-link' },
|
||||
img({ class: 'inhabitant-qr-small', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=120`, alt: 'QR' }))
|
||||
: null,
|
||||
cv.contact ? p(userLink(cv.contact)) : null,
|
||||
cv.description ? p(...renderUrl(`${cv.description}`)) : null,
|
||||
(cv.personalSkills && cv.personalSkills.length)
|
||||
? div(
|
||||
cv.personalSkills.map(tag =>
|
||||
a({
|
||||
href: `/search?query=%23${encodeURIComponent(tag)}`,
|
||||
class: "tag-link",
|
||||
style: "margin-right:0.8em;margin-bottom:0.5em;"
|
||||
}, `#${tag}`)
|
||||
)
|
||||
)
|
||||
: null
|
||||
]) : null,
|
||||
hasPersonal ? div({ class: "cv-box personal" }, ...[
|
||||
h2(i18n.cvLanguagesLabel),
|
||||
cv.languages ? p(`${cv.languages.toUpperCase()}`) : null
|
||||
]) : null,
|
||||
hasEducational ? div({ class: "cv-box education" }, ...[
|
||||
h2(i18n.cvEducationalView),
|
||||
cv.educationExperiences ? p(...renderUrl(`${cv.educationExperiences}`)) : null,
|
||||
(cv.educationalSkills && cv.educationalSkills.length)
|
||||
? div(
|
||||
cv.educationalSkills.map(tag =>
|
||||
a({
|
||||
href: `/search?query=%23${encodeURIComponent(tag)}`,
|
||||
class: "tag-link",
|
||||
style: "margin-right:0.8em;margin-bottom:0.5em;"
|
||||
}, `#${tag}`)
|
||||
)
|
||||
)
|
||||
: null
|
||||
]) : null,
|
||||
hasProfessional ? div({ class: "cv-box professional" }, ...[
|
||||
h2(i18n.cvProfessionalView),
|
||||
cv.professionalExperiences ? p(...renderUrl(`${cv.professionalExperiences}`)) : null,
|
||||
(cv.professionalSkills && cv.professionalSkills.length)
|
||||
? div(
|
||||
cv.professionalSkills.map(tag =>
|
||||
a({
|
||||
href: `/search?query=%23${encodeURIComponent(tag)}`,
|
||||
class: "tag-link",
|
||||
style: "margin-right:0.8em;margin-bottom:0.5em;"
|
||||
}, `#${tag}`)
|
||||
)
|
||||
)
|
||||
: null
|
||||
]) : null,
|
||||
hasOasis ? div({ class: "cv-box oasis" }, ...[
|
||||
h2(i18n.cvOasisContributorView),
|
||||
cv.oasisExperiences ? p(...renderUrl(`${cv.oasisExperiences}`)) : null,
|
||||
(cv.oasisSkills && cv.oasisSkills.length)
|
||||
? div(
|
||||
cv.oasisSkills.map(tag =>
|
||||
a({
|
||||
href: `/search?query=%23${encodeURIComponent(tag)}`,
|
||||
class: "tag-link",
|
||||
style: "margin-right:0.8em;margin-bottom:0.5em;"
|
||||
}, `#${tag}`)
|
||||
)
|
||||
)
|
||||
: null
|
||||
]) : null,
|
||||
hasAvailability ? div({ class: "cv-box availability" }, ...[
|
||||
h2(i18n.cvAvailabilityView),
|
||||
cv.location ? p(`${i18n.cvLocationLabel}: ${cv.location}`) : null,
|
||||
cv.status ? p(`${i18n.cvStatusLabel}: ${cv.status}`) : null,
|
||||
cv.preferences ? p(`${i18n.cvPreferencesLabel}: ${cv.preferences}`) : null
|
||||
]) : null
|
||||
])
|
||||
)
|
||||
div({ class: "tribe-details" }, cvSide, cvMain)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
172
src/views/data_view.js
Normal file
172
src/views/data_view.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink } = require("./main_views");
|
||||
|
||||
const KIND_LABEL = {
|
||||
inhabitants: () => i18n.dataKindInhabitants,
|
||||
jobs: () => i18n.dataKindJobs,
|
||||
projects: () => i18n.dataKindProjects,
|
||||
events: () => i18n.dataKindEvents,
|
||||
tribes: () => i18n.dataKindTribes,
|
||||
market: () => i18n.dataKindMarket,
|
||||
housing: () => i18n.dataKindHousing,
|
||||
industry: () => i18n.dataKindIndustry,
|
||||
tasks: () => i18n.dataKindTasks,
|
||||
reports: () => i18n.dataKindReports,
|
||||
votes: () => i18n.dataKindVotes,
|
||||
audios: () => i18n.audioTitle,
|
||||
videos: () => i18n.videoTitle,
|
||||
images: () => i18n.imageTitle,
|
||||
documents: () => i18n.documentTitle,
|
||||
bookmarks: () => i18n.bookmarkTitle,
|
||||
torrents: () => i18n.typeTorrent,
|
||||
chats: () => i18n.chatsTitle,
|
||||
pads: () => i18n.padsTitle,
|
||||
maps: () => i18n.mapTitle,
|
||||
calendars: () => i18n.calendarsTitle,
|
||||
forum: () => i18n.forumTitle
|
||||
};
|
||||
|
||||
const FILTER_COLUMNS = [
|
||||
["ALL", "RECENT", "TOP"],
|
||||
["INHABITANTS", "TRIBES"],
|
||||
["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS"],
|
||||
["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY"],
|
||||
["FORUM", "CHATS", "PADS", "MAPS"],
|
||||
["AUDIOS", "BOOKMARKS", "DOCUMENTS", "IMAGES", "TORRENTS", "VIDEOS"]
|
||||
];
|
||||
|
||||
const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase());
|
||||
|
||||
const filterLabel = (key) => {
|
||||
const map = {
|
||||
ALL: i18n.dataFilterAll, MINE: i18n.dataFilterMine,
|
||||
RECENT: i18n.dataFilterRecent, TOP: i18n.dataFilterTop
|
||||
};
|
||||
return map[key] || kindLabel(key.toLowerCase());
|
||||
};
|
||||
|
||||
const filterButton = (mode, current, q) =>
|
||||
form({ method: "GET", action: "/data" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
button({
|
||||
type: "submit", name: "filter", value: mode,
|
||||
class: current === mode ? "filter-btn active" : "filter-btn"
|
||||
}, String(filterLabel(mode)).toUpperCase())
|
||||
);
|
||||
|
||||
const renderFilters = (current, q) =>
|
||||
section(
|
||||
div({ class: "activity-filter-grid" },
|
||||
...FILTER_COLUMNS.map(col =>
|
||||
div({ class: "activity-filter-col" },
|
||||
...col.map(mode => filterButton(mode, current, q))
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: "data-search" },
|
||||
form({ method: "GET", action: "/data", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: current }),
|
||||
input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const scorePct = (score) => ((Number(score) || 0) * 100).toFixed(1).replace(/\.0$/, '');
|
||||
const scaleOf = (pct) => Math.min(4, Math.floor((Number(pct) || 0) / 20));
|
||||
|
||||
const renderCohesion = (c) => {
|
||||
const measurable = c.entities >= 2;
|
||||
const pct = measurable ? Number(c.percent) || 0 : 0;
|
||||
return section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.dataCohesionTitle),
|
||||
p(i18n.dataCohesionHint)
|
||||
),
|
||||
div({ class: "data-cohesion" },
|
||||
p({ class: "data-cohesion-value" },
|
||||
span({ class: "card-label data-cohesion-label" }, `${i18n.dataCcShort}: `),
|
||||
measurable ? `${c.percent}%` : "—"),
|
||||
div({ class: "poll-bar data-cohesion-bar" },
|
||||
div({ class: `poll-bar-fill poll-bar-fill-${Math.round(pct / 5) * 5} data-scale-${scaleOf(pct)}` })),
|
||||
div({ class: "data-cohesion-figures" },
|
||||
div({ class: "data-figure" },
|
||||
span({ class: "data-figure-value" }, String(c.entities)),
|
||||
span({ class: "data-figure-label" }, i18n.dataStatEntities)
|
||||
),
|
||||
div({ class: "data-figure" },
|
||||
span({ class: "data-figure-value" }, String(c.distinctTerms)),
|
||||
span({ class: "data-figure-label" }, i18n.dataStatTerms)
|
||||
),
|
||||
div({ class: "data-figure" },
|
||||
span({ class: "data-figure-value" }, String(c.pairs)),
|
||||
span({ class: "data-figure-label" }, i18n.dataStatPairs)
|
||||
),
|
||||
div({ class: "data-figure" },
|
||||
span({ class: "data-figure-value" }, String(c.isolated || 0)),
|
||||
span({ class: "data-figure-label" }, i18n.dataStatIsolated)
|
||||
),
|
||||
div({ class: "data-figure" },
|
||||
span({ class: "data-figure-value" }, String(c.connected || 0)),
|
||||
span({ class: "data-figure-label" }, i18n.dataStatConnected)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const shortId = (id) => {
|
||||
const value = String(id || "");
|
||||
return value.length > 14 ? `${value.slice(0, 12)}…` : value;
|
||||
};
|
||||
|
||||
const renderMatchRow = (m, isBest) => {
|
||||
const common = Array.isArray(m.common) ? m.common : [];
|
||||
return div({ class: isBest ? "data-card data-best-card" : "data-card" },
|
||||
div({ class: "data-side-top" },
|
||||
span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()),
|
||||
span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `),
|
||||
span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`),
|
||||
a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, "↗")
|
||||
),
|
||||
a({ href: m.href, class: "data-card-title" }, m.title || shortId(m.id)),
|
||||
span({ class: "data-card-author" }, userLink(m.author)),
|
||||
common.length
|
||||
? div({ class: "data-common-row" },
|
||||
span({ class: "card-label" }, `${i18n.dataCommonTerms}: `),
|
||||
...common.slice(0, 8).map(t =>
|
||||
a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))
|
||||
)
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
exports.dataView = async (payload = {}) => {
|
||||
const filter = String(payload.filter || 'ALL').toUpperCase();
|
||||
const q = payload.q || '';
|
||||
|
||||
const matches = Array.isArray(payload.matches) ? payload.matches : [];
|
||||
|
||||
return template(
|
||||
i18n.dataTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.dataTitle), p(i18n.dataDescription))),
|
||||
renderFilters(filter, q),
|
||||
payload.cohesion ? renderCohesion(payload.cohesion) : null,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(q ? `${i18n.dataTopicTitle} #${q}` : i18n.dataMatchesTitle),
|
||||
p(q ? i18n.dataTopicHint : i18n.dataMatchesHint),
|
||||
typeof payload.total === "number" && payload.total > matches.length
|
||||
? p({ class: "data-cohesion-hint" }, `${matches.length} / ${payload.total}`)
|
||||
: null
|
||||
),
|
||||
matches.length
|
||||
? div({ class: "data-list" }, ...matches.map((m, idx) => renderMatchRow(m, idx === 0)))
|
||||
: p({ class: "no-content" }, payload.hasProfile === false
|
||||
? i18n.dataNoProfile
|
||||
: i18n.dataNoMatches)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
const { form, button, div, h2, p, section, input, label, br, a, span, textarea, select, option } =
|
||||
require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
||||
|
|
@ -33,21 +34,6 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const renderFavoriteToggle = (doc, returnTo) =>
|
||||
form(
|
||||
{
|
||||
method: "POST",
|
||||
action: doc.isFavorite
|
||||
? `/documents/favorites/remove/${encodeURIComponent(doc.key)}`
|
||||
: `/documents/favorites/add/${encodeURIComponent(doc.key)}`
|
||||
},
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button(
|
||||
{ type: "submit", class: "filter-btn" },
|
||||
doc.isFavorite ? i18n.documentRemoveFavoriteButton : i18n.documentAddFavoriteButton
|
||||
)
|
||||
);
|
||||
|
||||
const renderDocumentActions = (filter, doc, params = {}) => {
|
||||
const returnTo = buildReturnTo(filter, params);
|
||||
const isAuthor = String(doc.author) === String(userId);
|
||||
|
|
@ -73,70 +59,11 @@ const renderDocumentActions = (filter, doc, params = {}) => {
|
|||
};
|
||||
|
||||
const renderDocumentCommentsSection = (documentKey, rootId, comments = [], returnTo = null) => {
|
||||
const list = safeArr(comments).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
return renderSharedCommentsSection({
|
||||
action: `/documents/${encodeURIComponent(documentKey)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
const commentsCount = list.length;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/documents/${encodeURIComponent(documentKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null,
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || "";
|
||||
const ts = c?.value?.timestamp || c?.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
|
||||
const content = c?.value?.content || {};
|
||||
const text = content.text || "";
|
||||
const threadRoot = content.fork || content.root || null;
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && threadRoot
|
||||
? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate)
|
||||
: ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
);
|
||||
};
|
||||
|
||||
const renderDocumentList = exports.renderDocumentList = (documents, filter, params = {}) => {
|
||||
|
|
@ -163,49 +90,19 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`)
|
||||
renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { spread: (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads || null, author: doc.author, favKind: 'documents', isFavorite: doc.isFavorite, reportTitle: doc.title })
|
||||
),
|
||||
div(
|
||||
{ class: "card-section document-card-body" },
|
||||
div(
|
||||
{ class: "bookmark-topbar" },
|
||||
div(
|
||||
{ class: "bookmark-topbar-left" },
|
||||
renderFavoriteToggle(doc, returnTo),
|
||||
topbarLeft
|
||||
),
|
||||
renderDocumentActions(filter, doc, params)
|
||||
),
|
||||
title ? h2(title) : null,
|
||||
doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null,
|
||||
doc?.url
|
||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` })
|
||||
: p(i18n.documentNoFile),
|
||||
div(
|
||||
{ class: "card-comments-summary" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ":"),
|
||||
span({ class: "card-value" }, String(commentCount)),
|
||||
br(),
|
||||
br(),
|
||||
form(
|
||||
{ method: "GET", action: `/documents/${encodeURIComponent(doc.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "filter", value: filter || "all" }),
|
||||
params.q ? input({ type: "hidden", name: "q", value: params.q }) : null,
|
||||
params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton)
|
||||
)
|
||||
renderEngagement(doc.key,
|
||||
renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants),
|
||||
renderCommentsLink({ href: `/documents/${encodeURIComponent(doc.key)}`, count: commentCount })
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(Object.values(doc.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)))
|
||||
),
|
||||
renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants)
|
||||
),
|
||||
div({ class: "card-spread-left" }, renderSpreadButton(doc.key, (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads)),
|
||||
br(),
|
||||
(() => {
|
||||
const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN;
|
||||
|
|
@ -214,12 +111,12 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(doc.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
|
@ -237,6 +134,7 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => {
|
|||
|
||||
return div(
|
||||
{ class: "div-center document-form" },
|
||||
params.spreadWarning || null,
|
||||
form(
|
||||
{
|
||||
action: filter === "edit" ? `/documents/update/${encodeURIComponent(documentId)}` : "/documents/create",
|
||||
|
|
@ -251,7 +149,7 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => {
|
|||
br(),
|
||||
label(i18n.documentTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }),
|
||||
br(),
|
||||
label(i18n.documentDescriptionLabel),
|
||||
br(),
|
||||
|
|
@ -268,20 +166,8 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => {
|
|||
};
|
||||
|
||||
exports.documentView = async (documents, filter = "all", documentId = null, params = {}) => {
|
||||
const title =
|
||||
filter === "mine"
|
||||
? i18n.documentMineSectionTitle
|
||||
: filter === "create"
|
||||
? i18n.documentCreateSectionTitle
|
||||
: filter === "edit"
|
||||
? i18n.documentUpdateSectionTitle
|
||||
: filter === "recent"
|
||||
? i18n.documentRecentSectionTitle
|
||||
: filter === "top"
|
||||
? i18n.documentTopSectionTitle
|
||||
: filter === "favorites"
|
||||
? i18n.documentFavoritesSectionTitle
|
||||
: i18n.documentAllSectionTitle;
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(documentId) };
|
||||
const title = i18n.documentTitle;
|
||||
|
||||
const q = safeText(params.q || "");
|
||||
const sort = safeText(params.sort || "recent");
|
||||
|
|
@ -308,14 +194,14 @@ exports.documentView = async (documents, filter = "all", documentId = null, para
|
|||
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase()),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.documentFilterFavorites
|
||||
String(i18n.documentFilterFavorites).toUpperCase()
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -334,9 +220,9 @@ exports.documentView = async (documents, filter = "all", documentId = null, para
|
|||
{ class: "filter-box__controls" },
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.documentSortRecent),
|
||||
option({ value: "oldest", selected: sort === "oldest" }, i18n.documentSortOldest),
|
||||
option({ value: "top", selected: sort === "top" }, i18n.documentSortTop)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.documentSortRecent),
|
||||
option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.documentSortOldest),
|
||||
option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.documentSortTop)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.documentSearchButton)
|
||||
)
|
||||
|
|
@ -368,7 +254,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
].filter(Boolean);
|
||||
|
||||
const sideActions = [];
|
||||
sideActions.push(renderFavoriteToggle(doc, returnTo));
|
||||
if (doc.author && String(doc.author) !== String(userId)) {
|
||||
sideActions.push(form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
|
|
@ -393,6 +278,17 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
|
||||
const tagsNode = renderTags(doc.tags);
|
||||
|
||||
const detailActions = div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(doc.key, null, {
|
||||
author: doc.author,
|
||||
favKind: 'documents',
|
||||
isFavorite: doc.isFavorite,
|
||||
spread: params.spreads || null,
|
||||
returnTo,
|
||||
reportTitle: doc.title
|
||||
})
|
||||
);
|
||||
|
||||
const docSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
title ? h2({ class: "tribe-card-title" }, title) : null,
|
||||
|
|
@ -403,11 +299,11 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
? p({ class: "tribe-side-description" }, ...renderUrl(doc.description))
|
||||
: null,
|
||||
tagsNode,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(doc.key, params.spreads)),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
);
|
||||
|
||||
const docMain = div({ class: "tribe-main" },
|
||||
detailActions,
|
||||
doc?.url
|
||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` })
|
||||
: p(i18n.documentNoFile),
|
||||
|
|
@ -418,18 +314,20 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(doc.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
})(),
|
||||
renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants),
|
||||
renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo)
|
||||
renderEngagement(doc.key,
|
||||
renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants),
|
||||
renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo)
|
||||
)
|
||||
);
|
||||
|
||||
const tpl = template(
|
||||
|
|
@ -445,14 +343,14 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase()),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.documentFilterFavorites
|
||||
String(i18n.documentFilterFavorites).toUpperCase()
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views");
|
||||
const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions } = require("./main_views");
|
||||
const { renderPhotoGallery, renderGalleryFields } = require("./gallery_view");
|
||||
const { renderIntervalBlock } = require("./calendars_view");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
|
@ -18,7 +21,7 @@ exports.renderEventInvitePage = (code) => {
|
|||
};
|
||||
|
||||
const opt = (value, isSelected, text) =>
|
||||
option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text);
|
||||
option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text);
|
||||
|
||||
const safeArray = (v) => (Array.isArray(v) ? v : []);
|
||||
|
||||
|
|
@ -105,70 +108,12 @@ const renderEventAttendAction = (e, isAttending, returnTo) => {
|
|||
};
|
||||
|
||||
const renderEventCommentsSection = (eventId, comments = [], currentFilter = "all") => {
|
||||
const commentsCount = Array.isArray(comments) ? comments.length : 0;
|
||||
const returnTo = `/events/${encodeURIComponent(eventId)}?filter=${encodeURIComponent(currentFilter || "all")}`;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/events/${encodeURIComponent(eventId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
(() => {
|
||||
const visibleComments = (comments || []).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
});
|
||||
return visibleComments.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
visibleComments.map((c) => {
|
||||
const author = c.value && c.value.author ? c.value.author : "";
|
||||
const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
|
||||
const content = c.value && c.value.content ? c.value.content : {};
|
||||
const root = content.fork || content.root || "";
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(String(text)))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet);
|
||||
})()
|
||||
);
|
||||
return renderSharedCommentsSection({
|
||||
action: `/events/${encodeURIComponent(eventId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
const renderEventStatusChip = (status) => {
|
||||
|
|
@ -198,7 +143,7 @@ const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo) => {
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`)
|
||||
renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`, { spread: spreadInfo || null, author: e.organizer || e.author, favKind: 'events', isFavorite: e.isFavorite, reportTitle: e.title })
|
||||
),
|
||||
div({ class: "card-section event-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
|
|
@ -216,8 +161,7 @@ const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo) => {
|
|||
: null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(e.id, spreadInfo))
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -228,14 +172,12 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
const { renderReachChip: renderReachChipEvents } = require('./clearnet_view');
|
||||
const viewerClearnetEvents = !!(params.viewerPrefs && params.viewerPrefs.clearnetEvents);
|
||||
|
||||
const title =
|
||||
currentFilter === "mine" ? i18n.eventMineSectionTitle :
|
||||
currentFilter === "create" ? i18n.eventCreateSectionTitle :
|
||||
currentFilter === "edit" ? i18n.eventUpdateSectionTitle :
|
||||
i18n.eventAllSectionTitle;
|
||||
const title = i18n.eventsTitle;
|
||||
|
||||
const eventToEdit = list.find((e) => e.id === eventId) || {};
|
||||
const editTags = Array.isArray(eventToEdit.tags) ? eventToEdit.tags.filter(Boolean) : [];
|
||||
const formData = currentFilter === "edit" ? eventToEdit : (params.draft || {});
|
||||
const spreadWarning = currentFilter === "edit" ? await renderSpreadEditWarning(eventToEdit && eventToEdit.id) : null;
|
||||
const editTags = Array.isArray(formData.tags) ? formData.tags.filter(Boolean) : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []);
|
||||
|
||||
const canSee = (e) => {
|
||||
const isPub = normalizePrivacy(e.isPublic) === "public";
|
||||
|
|
@ -270,7 +212,12 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm");
|
||||
|
||||
const ret = typeof returnTo === "string" && returnTo.startsWith("/events") ? returnTo : "/events?filter=mine";
|
||||
const editPrivacy = normalizePrivacy(eventToEdit.isPublic);
|
||||
const editPrivacy = normalizePrivacy(formData.isPublic);
|
||||
const editInterval = formData.interval
|
||||
|| (formData.intervalWeekly ? "weekly" : formData.intervalMonthly ? "monthly" : formData.intervalYearly ? "yearly" : "");
|
||||
const editIntervalUntil = (formData.recurrenceUntil || formData.intervalDeadline)
|
||||
? moment(formData.recurrenceUntil || formData.intervalDeadline).format("YYYY-MM-DDTHH:mm")
|
||||
: "";
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -285,21 +232,33 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/events" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday),
|
||||
button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek),
|
||||
button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth),
|
||||
button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear),
|
||||
button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived),
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
currentFilter === "edit" || currentFilter === "create"
|
||||
? null
|
||||
: div({ class: "filters" },
|
||||
form({ method: "GET", action: "/events", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: currentFilter }),
|
||||
input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.eventSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
currentFilter === "edit" || currentFilter === "create"
|
||||
? div(
|
||||
{ class: "event-form" },
|
||||
spreadWarning,
|
||||
form(
|
||||
{
|
||||
action: currentFilter === "edit" ? `/events/update/${encodeURIComponent(eventId)}` : "/events/create",
|
||||
|
|
@ -311,23 +270,20 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
br(),
|
||||
input({
|
||||
type: "text",
|
||||
name: "title",
|
||||
name: "title", maxlength: "100",
|
||||
id: "title",
|
||||
required: true,
|
||||
value: currentFilter === "edit" ? eventToEdit.title || "" : ""
|
||||
value: formData.title || ""
|
||||
}),
|
||||
br(),
|
||||
label(i18n.eventDescriptionLabel),
|
||||
br(),
|
||||
textarea(
|
||||
{ name: "description", id: "description", placeholder: i18n.eventDescriptionPlaceholder, rows: "4" },
|
||||
currentFilter === "edit" ? eventToEdit.description || "" : ""
|
||||
formData.description || ""
|
||||
),
|
||||
br(),
|
||||
label(i18n.uploadMedia),
|
||||
br(),
|
||||
input({ type: "file", name: "image", accept: "image/*" }),
|
||||
br(),
|
||||
...renderGalleryFields(formData, currentFilter === "edit"),
|
||||
br(),
|
||||
label(i18n.eventDateLabel),
|
||||
br(),
|
||||
|
|
@ -337,19 +293,10 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
id: "date",
|
||||
required: true,
|
||||
min: currentFilter === "create" ? minCreate : undefined,
|
||||
value: currentFilter === "edit" && eventToEdit.date ? moment(eventToEdit.date).format("YYYY-MM-DDTHH:mm") : ""
|
||||
value: formData.date ? moment(formData.date).format("YYYY-MM-DDTHH:mm") : ""
|
||||
}),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.eventPrivacyLabel),
|
||||
br(),
|
||||
select(
|
||||
{ name: "isPublic", id: "isPublic" },
|
||||
opt("public", editPrivacy !== "private", i18n.eventPublic),
|
||||
opt("private", editPrivacy === "private", i18n.eventPrivate)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.eventLocationLabel),
|
||||
br(),
|
||||
input({
|
||||
|
|
@ -357,17 +304,17 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
name: "location",
|
||||
id: "location",
|
||||
required: true,
|
||||
value: currentFilter === "edit" ? eventToEdit.location || "" : ""
|
||||
value: formData.location || ""
|
||||
}),
|
||||
br(),
|
||||
label(i18n.mapLocationTitle || "Map Location"),
|
||||
br(),
|
||||
input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: eventToEdit?.mapUrl || "" }),
|
||||
input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: formData.mapUrl || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.eventUrlLabel),
|
||||
br(),
|
||||
input({ type: "url", name: "url", id: "url", value: currentFilter === "edit" ? eventToEdit.url || "" : "" }),
|
||||
input({ type: "url", name: "url", id: "url", value: formData.url || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.eventPriceLabel),
|
||||
|
|
@ -378,13 +325,28 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => {
|
|||
id: "price",
|
||||
min: "0.000000",
|
||||
step: "0.000001",
|
||||
value: currentFilter === "edit" ? parseFloat(eventToEdit.price || 0).toFixed(6) : (0).toFixed(6)
|
||||
value: parseFloat(formData.price || 0).toFixed(6)
|
||||
}),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.eventTagsLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", id: "tags", value: currentFilter === "edit" ? editTags.join(", ") : "" }),
|
||||
input({ type: "text", name: "tags", id: "tags", value: editTags.join(", ") }),
|
||||
br(),
|
||||
br(),
|
||||
div({ class: "event-recurrence-block" },
|
||||
label(i18n.eventRecurrenceLabel),
|
||||
br(),
|
||||
renderIntervalBlock({ min: minCreate }, editInterval, editIntervalUntil)
|
||||
),
|
||||
br(),
|
||||
label(i18n.eventPrivacyLabel),
|
||||
br(),
|
||||
select(
|
||||
{ name: "isPublic", id: "isPublic" },
|
||||
opt("public", editPrivacy !== "private", i18n.eventPublic),
|
||||
opt("private", editPrivacy === "private", i18n.eventPrivate)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
...(currentFilter === "create" ? [
|
||||
|
|
@ -419,13 +381,13 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/events" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday),
|
||||
button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek),
|
||||
button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth),
|
||||
button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear),
|
||||
button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived),
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton)
|
||||
)
|
||||
);
|
||||
|
|
@ -455,20 +417,14 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => {
|
|||
].filter(Boolean);
|
||||
|
||||
const sideActions = [];
|
||||
if (event.organizer && event.organizer !== userId) {
|
||||
sideActions.push(form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: event.organizer }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
));
|
||||
}
|
||||
const attendNode = renderEventAttendAction(event, isAttending, returnToSelf);
|
||||
if (attendNode) sideActions.push(attendNode);
|
||||
if (params.linkedCalendarId) {
|
||||
sideActions.push(form({ method: "GET", action: `/calendars/${encodeURIComponent(params.linkedCalendarId)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.eventVisitCalendar || "Visit calendar")
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.eventVisitCalendar || "Visit calendar")
|
||||
));
|
||||
}
|
||||
if (isOrganizer) sideActions.push(...renderEventOwnerActions(event, returnToSelf));
|
||||
const ownerActions = isOrganizer ? renderEventOwnerActions(event, returnToSelf) : [];
|
||||
|
||||
const tagsNode = event.tags && event.tags.filter(Boolean).length
|
||||
? div({ class: "card-tags" },
|
||||
|
|
@ -482,7 +438,16 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => {
|
|||
td({ class: "tribe-info-label" }, labelText),
|
||||
td({ class: "tribe-info-value" }, valueNode)
|
||||
));
|
||||
if (event.date) pushRow(i18n.eventDateLabel, moment(event.date).format("YYYY/MM/DD HH:mm"));
|
||||
if (event.date) pushRow(i18n.eventDateLabel, moment(event.recurring && event.nextDate ? event.nextDate : event.date).format("YYYY/MM/DD HH:mm"));
|
||||
if (event.recurring) {
|
||||
const parts = [
|
||||
event.intervalWeekly ? (i18n.calendarIntervalWeekly || "Weekly") : null,
|
||||
event.intervalMonthly ? (i18n.calendarIntervalMonthly || "Monthly") : null,
|
||||
event.intervalYearly ? (i18n.calendarIntervalYearly || "Yearly") : null
|
||||
].filter(Boolean);
|
||||
pushRow(i18n.eventRecurrenceLabel, parts.join(" · "));
|
||||
if (event.recurrenceUntil) pushRow(i18n.eventRecurrenceUntil, moment(event.recurrenceUntil).format("YYYY/MM/DD HH:mm"));
|
||||
}
|
||||
pushRow(i18n.eventStatus, eventStatusLabel(event.status));
|
||||
pushRow(i18n.eventPrivacyLabel, privacyLabel(event.isPublic));
|
||||
if (event.location && String(event.location).trim()) pushRow(i18n.eventLocationLabel, event.location);
|
||||
|
|
@ -497,18 +462,31 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => {
|
|||
: null;
|
||||
|
||||
const eventSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(event.id, null, { spread: params.spreads || null, author: event.organizer || event.author, favKind: 'events', isFavorite: event.isFavorite, reportTitle: event.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, event.title)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(event.id, params.spreads)),
|
||||
renderPhotoGallery(event, 'event'),
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
event.recurring && Array.isArray(event.occurrences) && event.occurrences.length > 1
|
||||
? details({ class: "event-occurrences" },
|
||||
summary({ class: "chat-thread-summary" }, `${i18n.eventUpcomingDates} (${event.occurrences.length})`),
|
||||
ul({ class: "event-occurrence-list" },
|
||||
...event.occurrences.map(d => li(moment(d).format("YYYY/MM/DD HH:mm")))
|
||||
)
|
||||
)
|
||||
: null,
|
||||
tagsNode,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`)
|
||||
),
|
||||
attendeesListNode,
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null,
|
||||
ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null,
|
||||
renderDocumentActions('events', event.id)
|
||||
);
|
||||
|
||||
const returnToOpinions = `/events/${encodeURIComponent(event.id)}?filter=${encodeURIComponent(currentFilter)}`;
|
||||
|
|
@ -526,13 +504,13 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => {
|
|||
span({ class: "date-link" }, `${moment(event.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(event.organizer)
|
||||
),
|
||||
opinionsBar,
|
||||
renderEventCommentsSection(event.id, comments, currentFilter)
|
||||
renderEngagement(event.id, opinionsBar, renderEventCommentsSection(event.id, comments, currentFilter))
|
||||
);
|
||||
|
||||
return template(
|
||||
event.title,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(i18n.eventsTitle), p(i18n.eventsDescription)),
|
||||
filterBar,
|
||||
div({ class: "tribe-details" }, eventSide, eventMain)
|
||||
)
|
||||
|
|
@ -543,7 +521,7 @@ exports.clearnetEventView = async (event) => {
|
|||
const { escapeHtml: esc, renderClearnetPage } = require('./clearnet_view');
|
||||
const title = esc(event.title || 'Event');
|
||||
const desc = esc(event.description || '');
|
||||
const dateStr = event.date ? esc(moment(event.date).format("YYYY-MM-DD HH:mm")) : '';
|
||||
const dateStr = event.date ? esc(moment(event.date).format("YYYY/MM/DD HH:mm")) : '';
|
||||
const loc = esc(event.location || '');
|
||||
const price = parseFloat(event.price || 0);
|
||||
const priceStr = price > 0 ? `${price.toFixed(2)} ECO` : '';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
const { form, button, div, h2, p, section, input, label, br, a, img, span, textarea, select, option } =
|
||||
require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions } = require("./main_views");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions , renderSpreadEditWarning } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
const { renderMapLocationVisitLabel } = require("./maps_view");
|
||||
|
|
@ -22,18 +23,6 @@ const buildReturnTo = (filter, params = {}) => {
|
|||
return `/images?${parts.join("&")}`;
|
||||
};
|
||||
|
||||
const renderPMButton = (recipient, className = "filter-btn") => {
|
||||
const r = safeText(recipient);
|
||||
if (!r) return null;
|
||||
if (String(r) === String(userId)) return null;
|
||||
|
||||
return form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: r }),
|
||||
button({ type: "submit", class: className }, i18n.privateMessage)
|
||||
);
|
||||
};
|
||||
|
||||
const renderTags = (tags) => {
|
||||
const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean);
|
||||
return list.length
|
||||
|
|
@ -44,27 +33,12 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const renderImageFavoriteToggle = (imgObj, returnTo = "") =>
|
||||
form(
|
||||
{
|
||||
method: "POST",
|
||||
action: imgObj.isFavorite
|
||||
? `/images/favorites/remove/${encodeURIComponent(imgObj.key)}`
|
||||
: `/images/favorites/add/${encodeURIComponent(imgObj.key)}`
|
||||
},
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button(
|
||||
{ type: "submit", class: "filter-btn" },
|
||||
imgObj.isFavorite ? i18n.imageRemoveFavoriteButton : i18n.imageAddFavoriteButton
|
||||
)
|
||||
);
|
||||
|
||||
const renderImageMedia = (imgObj, filter, params = {}) => {
|
||||
const src = imgObj?.url ? `/blob/${encodeURIComponent(imgObj.url)}` : "";
|
||||
|
||||
return imgObj?.url
|
||||
? div(
|
||||
{ class: "image-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" },
|
||||
{ class: "image-container image-container-row" },
|
||||
a(
|
||||
{
|
||||
href: `/images/${encodeURIComponent(imgObj.key)}?filter=${encodeURIComponent(filter || "all")}${
|
||||
|
|
@ -111,7 +85,6 @@ const renderImageList = exports.renderImageList = (images, filter, params = {})
|
|||
? images.map((imgObj) => {
|
||||
const commentCount = typeof imgObj.commentCount === "number" ? imgObj.commentCount : 0;
|
||||
const title = safeText(imgObj.title);
|
||||
const ownerActions = renderImageOwnerActions(filter, imgObj, params);
|
||||
|
||||
const isOwn = imgObj.author && String(imgObj.author) === String(userId);
|
||||
return div(
|
||||
|
|
@ -119,50 +92,19 @@ const renderImageList = exports.renderImageList = (images, filter, params = {})
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`)
|
||||
renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads || null, author: imgObj.author, favKind: 'images', isFavorite: imgObj.isFavorite, reportTitle: imgObj.title })
|
||||
),
|
||||
div(
|
||||
{ class: "card-section image-card-body" },
|
||||
div(
|
||||
{ class: "bookmark-topbar" },
|
||||
div(
|
||||
{ class: "bookmark-topbar-left" },
|
||||
renderImageFavoriteToggle(imgObj, returnTo),
|
||||
renderPMButton(imgObj.author)
|
||||
),
|
||||
ownerActions.length ? div({ class: "bookmark-actions" }, ...ownerActions) : null
|
||||
),
|
||||
title ? h2(title) : null,
|
||||
(imgObj.meme || imgObj.lifetime) ? div({ class: "card-chips-row" },
|
||||
imgObj.meme ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) : null,
|
||||
imgObj.lifetime ? div({ class: "card-chips-row" },
|
||||
imgObj.lifetime ? renderLifespanChip(imgObj.lifetime, i18n) : null
|
||||
) : null,
|
||||
renderImageMedia(imgObj, filter, params),
|
||||
div(
|
||||
{ class: "card-comments-summary" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ":"),
|
||||
span({ class: "card-value" }, String(commentCount)),
|
||||
br(),
|
||||
br(),
|
||||
form(
|
||||
{ method: "GET", action: `/images/${encodeURIComponent(imgObj.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "filter", value: filter || "all" }),
|
||||
params.q ? input({ type: "hidden", name: "q", value: params.q }) : null,
|
||||
params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton)
|
||||
)
|
||||
renderEngagement(imgObj.key,
|
||||
renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants),
|
||||
renderCommentsLink({ href: `/images/${encodeURIComponent(imgObj.key)}`, count: commentCount })
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(Object.values(imgObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)))
|
||||
),
|
||||
renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants)
|
||||
),
|
||||
div({ class: "card-spread-left" }, renderSpreadButton(imgObj.key, (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads)),
|
||||
renderMapLocationVisitLabel(imgObj.mapUrl),
|
||||
br(),
|
||||
(() => {
|
||||
|
|
@ -172,12 +114,12 @@ const renderImageList = exports.renderImageList = (images, filter, params = {})
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(imgObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
|
@ -195,6 +137,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => {
|
|||
|
||||
return div(
|
||||
{ class: "div-center image-form" },
|
||||
params.spreadWarning || null,
|
||||
form(
|
||||
{
|
||||
action: filter === "edit" ? `/images/update/${encodeURIComponent(imageId)}` : "/images/create",
|
||||
|
|
@ -212,7 +155,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => {
|
|||
: null,
|
||||
label(i18n.imageTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }),
|
||||
br(),
|
||||
label(i18n.imageDescriptionLabel),
|
||||
br(),
|
||||
|
|
@ -222,21 +165,10 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => {
|
|||
br(),
|
||||
input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: imageToEdit?.mapUrl || "" }),
|
||||
br(),
|
||||
input({ type: "hidden", name: "meme", value: "0" }),
|
||||
label(i18n.imageTagsLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", placeholder: i18n.imageTagsPlaceholder, value: tagsValue }),
|
||||
br(),
|
||||
label(i18n.imageMemeLabel),
|
||||
input({
|
||||
id: "meme-checkbox",
|
||||
type: "checkbox",
|
||||
name: "meme",
|
||||
value: "1",
|
||||
class: "meme-checkbox",
|
||||
...(imageToEdit?.meme ? { checked: true } : {})
|
||||
}),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit" }, filter === "edit" ? i18n.imageUpdateButton : i18n.imageCreateButton)
|
||||
)
|
||||
|
|
@ -269,88 +201,16 @@ const renderLightbox = (images) =>
|
|||
});
|
||||
|
||||
const renderImageCommentsSection = (imageKey, comments = [], returnTo = null) => {
|
||||
const list = safeArr(comments).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
return renderSharedCommentsSection({
|
||||
action: `/images/${encodeURIComponent(imageKey)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
const commentsCount = list.length;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/images/${encodeURIComponent(imageKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || "";
|
||||
const ts = c?.value?.timestamp || c?.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
|
||||
const content = c?.value?.content || {};
|
||||
const text = content.text || "";
|
||||
const threadRoot = content.fork || content.root || null;
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && threadRoot ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
);
|
||||
};
|
||||
|
||||
exports.imageView = async (images, filter = "all", imageId = null, params = {}) => {
|
||||
const title =
|
||||
filter === "mine"
|
||||
? i18n.imageMineSectionTitle
|
||||
: filter === "create"
|
||||
? i18n.imageCreateSectionTitle
|
||||
: filter === "edit"
|
||||
? i18n.imageUpdateSectionTitle
|
||||
: filter === "gallery"
|
||||
? i18n.imageGallerySectionTitle
|
||||
: filter === "meme"
|
||||
? i18n.imageMemeSectionTitle
|
||||
: filter === "recent"
|
||||
? i18n.imageRecentSectionTitle
|
||||
: filter === "top"
|
||||
? i18n.imageTopSectionTitle
|
||||
: filter === "favorites"
|
||||
? i18n.imageFavoritesSectionTitle
|
||||
: i18n.imageAllSectionTitle;
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(imageId) };
|
||||
const title = i18n.imageTitle;
|
||||
|
||||
const q = safeText(params.q || "");
|
||||
const sort = safeText(params.sort || "recent");
|
||||
|
|
@ -377,19 +237,18 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {})
|
|||
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop),
|
||||
button(
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase()), button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.imageFilterFavorites
|
||||
String(i18n.imageFilterFavorites).toUpperCase()
|
||||
),
|
||||
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase()),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.imageFilterGallery
|
||||
String(i18n.imageFilterGallery).toUpperCase()
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -414,9 +273,9 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {})
|
|||
{ class: "filter-box__controls" },
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.imageSortRecent),
|
||||
option({ value: "oldest", selected: sort === "oldest" }, i18n.imageSortOldest),
|
||||
option({ value: "top", selected: sort === "top" }, i18n.imageSortTop)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.imageSortRecent),
|
||||
option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.imageSortOldest),
|
||||
option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.imageSortTop)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.imageSearchButton)
|
||||
)
|
||||
|
|
@ -438,25 +297,28 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
|||
const { renderReachChip } = require('./clearnet_view');
|
||||
const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetImages);
|
||||
|
||||
const memeChip = imageObj.meme
|
||||
? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME"))
|
||||
: null;
|
||||
const chips = [
|
||||
memeChip,
|
||||
renderLifespanChip(imageObj.lifetime, i18n),
|
||||
imageObj.sizeBytes ? renderEcoTax(imageObj.sizeBytes, imageObj.key) : null
|
||||
].filter(Boolean);
|
||||
|
||||
const ownerActions = renderImageOwnerActions(filter, imageObj, { q, sort });
|
||||
const sideActions = [];
|
||||
sideActions.push(renderImageFavoriteToggle(imageObj, returnTo));
|
||||
if (imageObj.author && String(imageObj.author) !== String(userId)) {
|
||||
sideActions.push(renderPMButton(imageObj.author));
|
||||
}
|
||||
for (const a of ownerActions) sideActions.push(a);
|
||||
|
||||
const tagsNode = renderTags(imageObj.tags);
|
||||
|
||||
const detailActions = div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(imageObj.key, null, {
|
||||
author: imageObj.author,
|
||||
favKind: 'images',
|
||||
isFavorite: imageObj.isFavorite,
|
||||
spread: params.spreads || null,
|
||||
returnTo,
|
||||
reportTitle: imageObj.title
|
||||
})
|
||||
);
|
||||
|
||||
const imageSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
title ? h2({ class: "tribe-card-title" }, title) : null,
|
||||
|
|
@ -467,12 +329,12 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
|||
? p({ class: "tribe-side-description" }, ...renderUrl(imageObj.description))
|
||||
: null,
|
||||
tagsNode,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(imageObj.key, params.spreads)),
|
||||
renderMapLocationVisitLabel(imageObj.mapUrl),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
);
|
||||
|
||||
const imageMain = div({ class: "tribe-main" },
|
||||
detailActions,
|
||||
imageObj?.url
|
||||
? div(
|
||||
{ class: "image-container" },
|
||||
|
|
@ -491,18 +353,20 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(imageObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
})(),
|
||||
renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants),
|
||||
renderImageCommentsSection(imageObj.key, comments, returnTo)
|
||||
renderEngagement(imageObj.key,
|
||||
renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants),
|
||||
renderImageCommentsSection(imageObj.key, comments, returnTo)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
|
|
@ -518,19 +382,18 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
|||
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop),
|
||||
button(
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase()), button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.imageFilterFavorites
|
||||
String(i18n.imageFilterFavorites).toUpperCase()
|
||||
),
|
||||
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase()),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.imageFilterGallery
|
||||
String(i18n.imageFilterGallery).toUpperCase()
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, ul, li, img, video, audio, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views")
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions , renderSpreadEditWarning } = require("./main_views")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderMapEmbedWithZoom } = require("./maps_view")
|
||||
|
|
@ -120,7 +120,7 @@ const renderFacilityList = exports.renderFacilityList = (facilities, filter, spr
|
|||
return div({ class: "trending-card tribes-card industry-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(fc.id || fc.key, href)
|
||||
renderContentActions(fc.id || fc.key, href, { spread: spreadMap.get(fc.id || fc.key) || null })
|
||||
),
|
||||
div({ class: "card-section tribes-card-body" },
|
||||
div({ class: "tribe-card-image-wrapper" },
|
||||
|
|
@ -140,7 +140,6 @@ const renderFacilityList = exports.renderFacilityList = (facilities, filter, spr
|
|||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(fc.id || fc.key, spreadMap.get(fc.id || fc.key)))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -148,13 +147,14 @@ const renderFacilityList = exports.renderFacilityList = (facilities, filter, spr
|
|||
)
|
||||
}
|
||||
|
||||
const renderFacilityForm = (facility, mode) => {
|
||||
const renderFacilityForm = (facility, mode, spreadWarning = null) => {
|
||||
const fc = facility || {}
|
||||
const isEdit = mode === "edit"
|
||||
const returnTo = "/industry?filter=MINE"
|
||||
const curPolicy = fc.membershipPolicy || "vote"
|
||||
const curSector = fc.sector || "other"
|
||||
return div({ class: "div-center industry-form" },
|
||||
isEdit ? spreadWarning : null,
|
||||
form({
|
||||
action: isEdit ? `/industry/update/${encodeURIComponent(fc.id)}` : "/industry/create",
|
||||
method: "POST",
|
||||
|
|
@ -284,8 +284,8 @@ const renderBuildCard = (b, opts = {}) => {
|
|||
b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null,
|
||||
b.notes ? p({ class: "tribe-card-description" }, ...renderUrl(safeText(b.notes))) : null,
|
||||
div({ class: "industry-blueprint-meta industry-dates-row" },
|
||||
b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY-MM-DD")) : null,
|
||||
b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY-MM-DD")) : null,
|
||||
b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null,
|
||||
b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null,
|
||||
),
|
||||
renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"),
|
||||
p(span({ class: "industry-meta-label" }, `${i18n.industryContributions || "Contributions"}: `), String(safeArr(b.contributions).length)),
|
||||
|
|
@ -308,6 +308,7 @@ const renderGlobalBuilds = (builds, spreadMap = new Map()) => {
|
|||
|
||||
exports.industryView = async (facilitiesOrForm, filter, params = {}) => {
|
||||
const f = String(filter || "ALL").toUpperCase()
|
||||
const facilityEditWarning = f === "EDIT" ? await renderSpreadEditWarning((safeArr(facilitiesOrForm)[0] || {}).id) : null
|
||||
const search = safeText(params.search)
|
||||
const sectorSel = safeText(params.sector)
|
||||
const isForm = f === "CREATE" || f === "EDIT"
|
||||
|
|
@ -324,14 +325,14 @@ exports.industryView = async (facilitiesOrForm, filter, params = {}) => {
|
|||
form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "search", value: search }),
|
||||
input({ type: "hidden", name: "sector", value: sectorSel }),
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n] || x.key))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility"))
|
||||
)
|
||||
),
|
||||
isRules
|
||||
? renderIndustryRules()
|
||||
: isForm
|
||||
? renderFacilityForm(f === "EDIT" ? (safeArr(facilitiesOrForm)[0] || {}) : {}, f === "EDIT" ? "edit" : "create")
|
||||
? renderFacilityForm(f === "EDIT" ? (safeArr(facilitiesOrForm)[0] || {}) : {}, f === "EDIT" ? "edit" : "create", facilityEditWarning)
|
||||
: f === "BLUEPRINTS"
|
||||
? div({ class: "industry-list" }, renderGlobalBlueprints(facilitiesOrForm, params.spreadMap || new Map()))
|
||||
: f === "BUILDS"
|
||||
|
|
@ -346,7 +347,7 @@ exports.industryView = async (facilitiesOrForm, filter, params = {}) => {
|
|||
[option({ value: "", ...(sectorSel ? {} : { selected: true }) }, i18n.industryAllSectors || "All sectors")]
|
||||
.concat(SECTORS.map((s) => option({ value: s, ...(s === sectorSel ? { selected: true } : {}) }, sectorLabel(s))))
|
||||
),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.applyFilters || "Apply")
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -507,10 +508,9 @@ const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map()
|
|||
const cards = list.map((bp) => {
|
||||
const actions = renderBlueprintGovernActions(fc.id, bp, isMember, `/industry/${encodeURIComponent(fc.id)}`)
|
||||
return div({ class: "industry-card-wrap" },
|
||||
div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`)),
|
||||
div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { spread: spreadMap.get(bp.id) || null })),
|
||||
div({ class: "industry-blueprint-card" },
|
||||
div({ class: "card-chips-row" },
|
||||
renderSpreadButton(bp.id, spreadMap.get(bp.id)),
|
||||
renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind),
|
||||
renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase())
|
||||
),
|
||||
|
|
@ -532,12 +532,13 @@ const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map()
|
|||
)
|
||||
}
|
||||
|
||||
const renderBlueprintForm = (fc, bp, mode) => {
|
||||
const renderBlueprintForm = (fc, bp, mode, spreadWarning = null) => {
|
||||
const isEdit = mode === "edit"
|
||||
const curKind = bp.outKind || "physical"
|
||||
const materialsText = safeArr(bp.materials).map(m => `${m.item}:${m.qty}:${m.price != null ? m.price : 0}`).join("\n")
|
||||
return div({ class: "industry-form industry-blueprint-form" },
|
||||
h2(isEdit ? (i18n.industryEditBlueprint || "Edit blueprint") : (i18n.industryNewBlueprint || "New blueprint")),
|
||||
isEdit ? spreadWarning : null,
|
||||
form({ method: "POST", action: isEdit ? `/industry/blueprints/${encodeURIComponent(bp.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/blueprints`, enctype: "multipart/form-data" },
|
||||
label(i18n.industryName || "Name"),
|
||||
br(), input({ type: "text", name: "name", required: true, maxlength: "80", value: bp.name || "" }), br(),
|
||||
|
|
@ -558,12 +559,13 @@ const renderBlueprintForm = (fc, bp, mode) => {
|
|||
)
|
||||
}
|
||||
|
||||
const renderBuildForm = (fc, blueprints, build, mode) => {
|
||||
const renderBuildForm = (fc, blueprints, build, mode, spreadWarning = null) => {
|
||||
const b = build || {}
|
||||
const isEdit = mode === "edit"
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return div({ class: "industry-form industry-build-form" },
|
||||
h2(isEdit ? (i18n.industryEditBuild || "Edit build") : (i18n.industryNewBuild || "Propose a build")),
|
||||
isEdit ? spreadWarning : null,
|
||||
form({ method: "POST", action: isEdit ? `/industry/builds/${encodeURIComponent(b.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/builds`, enctype: "multipart/form-data" },
|
||||
label(i18n.industryBuildTitle || "Title"),
|
||||
br(), input({ type: "text", name: "title", required: true, maxlength: "120", value: safeText(b.title) }), br(),
|
||||
|
|
@ -630,11 +632,11 @@ const renderFacilitySide = (fc, returnTo, params = {}) => {
|
|||
}
|
||||
if (isSteward) {
|
||||
sideActions.push(form({ method: "GET", action: `/industry/edit/${encodeURIComponent(fc.id)}` },
|
||||
button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.industryUpdateButton || "Update")
|
||||
))
|
||||
if (memberCount <= 1) {
|
||||
sideActions.push(form({ method: "POST", action: `/industry/delete/${encodeURIComponent(fc.id)}` },
|
||||
button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.industryDeleteButton || "Delete")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -642,14 +644,13 @@ const renderFacilitySide = (fc, returnTo, params = {}) => {
|
|||
return div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/${encodeURIComponent(fc.id)}` }, safeText(fc.name) || (i18n.industryTitle || "Industry")))),
|
||||
div({ class: "card-chips-row" }, ...chips),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(fc.id || fc.key, params.spreads)),
|
||||
fc.image ? renderMediaBlob(fc.image, { class: "tribe-detail-image" }) : null,
|
||||
fc.description ? p({ class: "tribe-side-description" }, ...renderUrl(safeText(fc.description))) : null,
|
||||
table({ class: "tribe-info-table" },
|
||||
tr(td({ class: "tribe-info-value tribe-author-cell", colspan: "4" }, userLink(fc.steward))),
|
||||
tr(td({ class: "tribe-info-label" }, i18n.industryLaborRate || "Labor rate"), td({ class: "tribe-info-value", colspan: "3" }, `${fc.laborRate || 0} ECO/h`)),
|
||||
tr(td({ class: "tribe-info-label" }, i18n.industryQuorum || "Quorum"), td({ class: "tribe-info-value", colspan: "3" }, String(fc.quorum))),
|
||||
tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`))
|
||||
tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`)),
|
||||
tr(td({ class: "tribe-info-value tribe-author-cell", colspan: "4" }, userLink(fc.steward)))
|
||||
),
|
||||
fc.mapUrl ? renderMapEmbedWithZoom(params.mapData, fc.mapUrl, returnTo, params.zoom) : null,
|
||||
safeArr(fc.tags).length
|
||||
|
|
@ -689,13 +690,12 @@ exports.singleFacilityView = async (facility, filter, params = {}) => {
|
|||
i18n.industryTitle || "Industry",
|
||||
section(
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(fc.id || fc.key, returnTo)
|
||||
renderContentActions(fc.id || fc.key, null, { spread: params.spreads || null })
|
||||
),
|
||||
div({ class: "tags-header" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")),
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n] || x.key))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility"))
|
||||
)
|
||||
),
|
||||
|
|
@ -708,17 +708,16 @@ exports.blueprintEditView = async (bp, fc) => template(
|
|||
i18n.industryTitle || "Industry",
|
||||
section(
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`)
|
||||
),
|
||||
div({ class: "tags-header" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")),
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, i18n[x.i18n] || x.key))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility"))
|
||||
)
|
||||
),
|
||||
renderBlueprintForm(fc, bp, "edit")
|
||||
renderBlueprintForm(fc, bp, "edit", await renderSpreadEditWarning(bp && bp.id))
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -726,17 +725,16 @@ exports.buildEditView = async (b, fc) => template(
|
|||
i18n.industryTitle || "Industry",
|
||||
section(
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(b.id, `/industry/build/${encodeURIComponent(b.id)}`)
|
||||
),
|
||||
div({ class: "tags-header" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")),
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, i18n[x.i18n] || x.key))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility"))
|
||||
)
|
||||
),
|
||||
renderBuildForm(fc, [], b, "edit")
|
||||
renderBuildForm(fc, [], b, "edit", await renderSpreadEditWarning(b && b.id))
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -749,25 +747,22 @@ exports.singleBlueprintView = async (blueprint, params = {}) => {
|
|||
|
||||
return template(
|
||||
i18n.industryTitle || "Industry",
|
||||
section(div({ class: "tags-header" }, h2(i18n.industryTitle), p(i18n.industryDescription))),
|
||||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, i18n[x.i18n] || x.key))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility"))
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "shop-detail" },
|
||||
(bp.author && String(bp.author) !== String(userId))
|
||||
? div({ class: "bookmark-topbar transfer-topbar-single" },
|
||||
div({ class: "bookmark-topbar-left" },
|
||||
form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: bp.author }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage))
|
||||
)
|
||||
)
|
||||
: null,
|
||||
div({ class: "card-chips-row" },
|
||||
renderSpreadButton(bp.id, params.spreads),
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(bp.id, null, { spread: params.spreads || null, author: bp.author, reportTitle: bp.name || bp.title })
|
||||
),
|
||||
div({ class: "card-chips-row" },
|
||||
renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind),
|
||||
renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase())
|
||||
),
|
||||
|
|
@ -915,8 +910,8 @@ exports.singleBuildView = async (build, params = {}) => {
|
|||
const productName = safeText(b.blueprintName) || safeText(b.title)
|
||||
const crossTags = [facilityName, productName].filter(Boolean).map(x => x.toLowerCase().replace(/\s+/g, "-")).join(", ")
|
||||
const crossDates = [
|
||||
b.startDate ? `${i18n.industryBuildStart || "Start date"}: ${moment(b.startDate).format("YYYY-MM-DD")}` : null,
|
||||
b.endDate ? `${i18n.industryBuildEnd || "End date"}: ${moment(b.endDate).format("YYYY-MM-DD")}` : null
|
||||
b.startDate ? `${i18n.industryBuildStart || "Start date"}: ${moment(b.startDate).format("YYYY/MM/DD")}` : null,
|
||||
b.endDate ? `${i18n.industryBuildEnd || "End date"}: ${moment(b.endDate).format("YYYY/MM/DD")}` : null
|
||||
].filter(Boolean).join(" · ")
|
||||
const crossContext = [
|
||||
`${i18n.industryFacility || "Facility"}: ${facilityName}`,
|
||||
|
|
@ -937,34 +932,32 @@ exports.singleBuildView = async (build, params = {}) => {
|
|||
|
||||
return template(
|
||||
i18n.industryTitle || "Industry",
|
||||
section(div({ class: "tags-header" }, h2(i18n.industryTitle), p(i18n.industryDescription))),
|
||||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, i18n[x.i18n] || x.key))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility"))
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "shop-detail" },
|
||||
(b.proposer && String(b.proposer) !== String(userId))
|
||||
? div({ class: "bookmark-topbar transfer-topbar-single" },
|
||||
div({ class: "bookmark-topbar-left" },
|
||||
form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: b.proposer }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage))
|
||||
)
|
||||
)
|
||||
: null,
|
||||
b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null,
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(b.id, null, { spread: params.spreads || null, author: b.proposer, reportTitle: b.title })
|
||||
),
|
||||
b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null,
|
||||
h2(safeText(b.title) || (i18n.industryBuild || "Build")),
|
||||
div({ class: "card-chips-row" }, renderSpreadButton(b.id, params.spreads), renderBuildStatusChip(status)),
|
||||
div({ class: "card-chips-row" }, renderBuildStatusChip(status)),
|
||||
div({ class: "industry-meta" },
|
||||
b.blueprintName ? p(span({ class: "industry-meta-label" }, `${i18n.industryBlueprint || "Blueprint"}: `), safeText(b.blueprintName)) : null,
|
||||
b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null,
|
||||
p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)),
|
||||
b.notes ? p({ class: "industry-build-notes" }, span({ class: "industry-meta-label" }, `${i18n.industryBuildNotes || "Notes"}: `), safeText(b.notes)) : null,
|
||||
div({ class: "industry-dates-row" },
|
||||
b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY-MM-DD")) : null,
|
||||
b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY-MM-DD")) : null,
|
||||
b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null,
|
||||
b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null,
|
||||
(b.endDate && status !== "COMPLETED" && status !== "FAILED")
|
||||
? p(span({ class: "industry-meta-label" }, `${i18n.industryTimeLeft || "Time left"}: `), `${Math.max(0, Math.ceil((new Date(b.endDate) - Date.now()) / 86400000))}d`)
|
||||
: null
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views")
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning } = require("./main_views")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view")
|
||||
const { resolvePhoto } = require("./inhabitants_view")
|
||||
|
||||
const renderMediaBlob = (value, attrs = {}) => {
|
||||
if (!value) return null
|
||||
|
|
@ -24,6 +26,8 @@ const userId = config.keys.id
|
|||
const FILTERS = [
|
||||
{ key: "ALL", i18n: "jobsFilterAll", title: "jobsAllTitle" },
|
||||
{ key: "MINE", i18n: "jobsFilterMine", title: "jobsMineTitle" },
|
||||
{ key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" },
|
||||
{ key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" },
|
||||
{ key: "APPLIED", i18n: "jobsFilterApplied", title: "jobsAppliedTitle" },
|
||||
{ key: "REMOTE", i18n: "jobsFilterRemote", title: "jobsRemoteTitle" },
|
||||
{ key: "PRESENCIAL", i18n: "jobsFilterPresencial", title: "jobsPresencialTitle" },
|
||||
|
|
@ -32,17 +36,9 @@ const FILTERS = [
|
|||
{ key: "EXCHANGE", i18n: "jobsFilterExchange", title: "jobsExchangeTitle" },
|
||||
{ key: "OPEN", i18n: "jobsFilterOpen", title: "jobsOpenTitle" },
|
||||
{ key: "CLOSED", i18n: "jobsFilterClosed", title: "jobsClosedTitle" },
|
||||
{ key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" },
|
||||
{ key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" },
|
||||
{ key: "CV", i18n: "jobsCV", title: "jobsCVTitle" }
|
||||
]
|
||||
|
||||
function resolvePhoto(photoField, size = 256) {
|
||||
if (typeof photoField === "string" && photoField.startsWith("/image/")) return photoField
|
||||
if (typeof photoField === "string" && /^&[A-Za-z0-9+/=]+\.sha256$/.test(photoField)) return `/image/${size}/${encodeURIComponent(photoField)}`
|
||||
return "/assets/images/default-avatar.png"
|
||||
}
|
||||
|
||||
const safeArr = (v) => (Array.isArray(v) ? v : [])
|
||||
const safeText = (v) => String(v || "").trim()
|
||||
|
||||
|
|
@ -109,7 +105,7 @@ const renderUpdatedLabel = (createdAt, updatedAt) => {
|
|||
const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN
|
||||
const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs)
|
||||
return showUpdated
|
||||
? span({ class: "votations-comment-date" }, ` | ${i18n.jobsUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm:ss")}`)
|
||||
? span({ class: "votations-comment-date" }, ` | ${i18n.jobsUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm")}`)
|
||||
: null
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +241,7 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => {
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`)
|
||||
renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreadMap && params.spreadMap.get(job.id) || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title })
|
||||
),
|
||||
div({ class: "card-section job-card-body" },
|
||||
heroNode,
|
||||
|
|
@ -259,8 +255,7 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => {
|
|||
div({ class: "card-date-highlight" }, compensationText),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreadMap && params.spreadMap.get(job.id)))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -268,10 +263,11 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const renderJobForm = (job = {}, mode = "create") => {
|
||||
const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => {
|
||||
const isEdit = mode === "edit"
|
||||
return div(
|
||||
{ class: "div-center job-form" },
|
||||
isEdit ? spreadWarning : null,
|
||||
form(
|
||||
{
|
||||
action: isEdit ? `/jobs/update/${encodeURIComponent(job.id)}` : "/jobs/create",
|
||||
|
|
@ -292,7 +288,7 @@ const renderJobForm = (job = {}, mode = "create") => {
|
|||
br(),
|
||||
label(i18n.jobTitle),
|
||||
br(),
|
||||
input({ type: "text", name: "title", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.jobImage),
|
||||
|
|
@ -325,8 +321,8 @@ const renderJobForm = (job = {}, mode = "create") => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "job_time", required: true },
|
||||
option({ value: "partial", selected: job.job_time === "partial" }, i18n.jobTimePartial),
|
||||
option({ value: "complete", selected: job.job_time === "complete" }, i18n.jobTimeComplete)
|
||||
option({ value: "partial", ...(job.job_time === "partial" ? { selected: true } : {})}, i18n.jobTimePartial),
|
||||
option({ value: "complete", ...(job.job_time === "complete" ? { selected: true } : {})}, i18n.jobTimeComplete)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
|
|
@ -339,8 +335,8 @@ const renderJobForm = (job = {}, mode = "create") => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "location", required: true },
|
||||
option({ value: "remote", selected: job.location === "remote" }, i18n.jobLocationRemote),
|
||||
option({ value: "presencial", selected: job.location === "presencial" }, i18n.jobLocationPresencial)
|
||||
option({ value: "remote", ...(job.location === "remote" ? { selected: true } : {})}, i18n.jobLocationRemote),
|
||||
option({ value: "presencial", ...(job.location === "presencial" ? { selected: true } : {})}, i18n.jobLocationPresencial)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
|
|
@ -353,8 +349,8 @@ const renderJobForm = (job = {}, mode = "create") => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "visibility" },
|
||||
option({ value: "PUBLIC", selected: (job.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"),
|
||||
option({ value: "HIDDEN", selected: job.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden")
|
||||
option({ value: "PUBLIC", ...((job.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"),
|
||||
option({ value: "HIDDEN", ...(job.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden")
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
|
|
@ -419,9 +415,23 @@ const renderCVList = (inhabitants) =>
|
|||
{ class: "inhabitant-details" },
|
||||
user.description ? p(...renderUrl(user.description)) : null,
|
||||
p(userLink(user.id)),
|
||||
user.skills && user.skills.length
|
||||
? div({ class: "card-tags" },
|
||||
user.skills.slice(0, 12).map((skill) =>
|
||||
a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`))
|
||||
)
|
||||
: null,
|
||||
typeof user.matchScore === "number"
|
||||
? div({ class: "cv-match-row" },
|
||||
span({ class: "card-label" }, `${i18n.matchScore}: `),
|
||||
span({ class: "card-value" }, `${Math.round(user.matchScore * 100)}%`)
|
||||
)
|
||||
: null,
|
||||
div(
|
||||
{ class: "cv-actions" },
|
||||
form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.inhabitantviewDetails)),
|
||||
form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.cvVisitButton)),
|
||||
form({ method: "GET", action: `/cv/pdf/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.generatePdf)),
|
||||
form({ method: "POST", action: `/cv/share/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)),
|
||||
!isMe ? renderPmButton(user.id) : null
|
||||
)
|
||||
)
|
||||
|
|
@ -431,6 +441,10 @@ const renderCVList = (inhabitants) =>
|
|||
)
|
||||
|
||||
exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => {
|
||||
if (filter === "EDIT") {
|
||||
const editing = Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : jobsOrCVs
|
||||
params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && editing.id) }
|
||||
}
|
||||
const search = safeText(params.search || "")
|
||||
const minSalary = params.minSalary ?? ""
|
||||
const maxSalary = params.maxSalary ?? ""
|
||||
|
|
@ -459,7 +473,7 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => {
|
|||
input({ type: "hidden", name: "maxSalary", value: String(maxSalary ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...FILTERS.map((f) =>
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n])
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase())
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob)
|
||||
)
|
||||
|
|
@ -492,7 +506,7 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => {
|
|||
...(params.tasks ? { tasks: params.tasks } : {}),
|
||||
...(params.salary ? { salary: params.salary } : {})
|
||||
}
|
||||
return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create")
|
||||
return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create", params.spreadWarning)
|
||||
})()
|
||||
: section(
|
||||
div(
|
||||
|
|
@ -510,9 +524,9 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => {
|
|||
),
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.jobsSortRecent),
|
||||
option({ value: "salary", selected: sort === "salary" }, i18n.jobsSortSalary),
|
||||
option({ value: "subscribers", selected: sort === "subscribers" }, i18n.jobsSortSubscribers)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.jobsSortRecent),
|
||||
option({ value: "salary", ...(sort === "salary" ? { selected: true } : {})}, i18n.jobsSortSalary),
|
||||
option({ value: "subscribers", ...(sort === "subscribers" ? { selected: true } : {})}, i18n.jobsSortSubscribers)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.jobsSearchButton)
|
||||
)
|
||||
|
|
@ -526,61 +540,12 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => {
|
|||
}
|
||||
|
||||
const renderJobCommentsSection = (jobId, returnTo, comments = []) => {
|
||||
const list = safeArr(comments).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text
|
||||
return t && String(t).trim()
|
||||
})
|
||||
const commentsCount = list.length
|
||||
|
||||
return div(
|
||||
{ id: "comments", class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/jobs/${encodeURIComponent(jobId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || ""
|
||||
const ts = c?.value?.timestamp || c?.timestamp
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""
|
||||
const relDate = ts ? moment(ts).fromNow() : ""
|
||||
const userName = author && author.includes("@") ? author.split("@")[1] : author
|
||||
const rootId = c?.value?.content ? (c.value.content.fork || c.value.content.root) : null
|
||||
const text = c?.value?.content?.text || ""
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
)
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
)
|
||||
}
|
||||
return renderSharedCommentsSection({
|
||||
action: `/jobs/${encodeURIComponent(jobId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
const renderCandidates = (candidates, jobId) => {
|
||||
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
||||
|
|
@ -643,7 +608,6 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {})
|
|||
|
||||
const ownerActions = renderJobOwnerActions(job, returnTo)
|
||||
const applyToggle = renderJobApplyToggle(job, returnTo)
|
||||
const pmBtn = renderPmButton(job.author)
|
||||
const visibility = (job.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'
|
||||
const chips = [
|
||||
renderJobStatusChip(job.status),
|
||||
|
|
@ -671,15 +635,16 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {})
|
|||
|
||||
const sideActions = []
|
||||
if (applyToggle) sideActions.push(applyToggle)
|
||||
if (pmBtn) sideActions.push(pmBtn)
|
||||
for (const a of ownerActions) sideActions.push(a)
|
||||
|
||||
const jobSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(job.id, null, { spread: params.spreads || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, safeText(job.title) || i18n.jobsTitle)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreads)),
|
||||
job.image
|
||||
? renderMediaBlob(job.image, { class: "tribe-detail-image" })
|
||||
: null,
|
||||
|
|
@ -720,6 +685,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {})
|
|||
|
||||
return template(
|
||||
i18n.jobsTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.jobsTitle), p(i18n.jobsDescription))),
|
||||
section(
|
||||
div(
|
||||
{ class: "filters" },
|
||||
|
|
@ -730,7 +696,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {})
|
|||
input({ type: "hidden", name: "maxSalary", value: String(params.maxSalary ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: safeText(params.sort || "recent") }),
|
||||
...FILTERS.map((f) =>
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n])
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase())
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -255,6 +255,34 @@ exports.i18n = i18n;
|
|||
Object.defineProperty(exports, 'selectedLanguage', { get: () => selectedLanguage });
|
||||
|
||||
const opinionCategoriesList = require('../backend/opinion_categories');
|
||||
const renderEngagement = (id, opinionsNode, commentsNode) => {
|
||||
const parts = [opinionsNode, commentsNode].filter(Boolean);
|
||||
if (!parts.length) return null;
|
||||
return div({ class: 'card-engage' }, ...parts);
|
||||
};
|
||||
exports.renderEngagement = renderEngagement;
|
||||
|
||||
let spreadsModel = null;
|
||||
const spreadsFor = async (msgId) => {
|
||||
if (!msgId || typeof msgId !== 'string' || !msgId.startsWith('%')) return 0;
|
||||
try {
|
||||
if (!spreadsModel) spreadsModel = require('../models/main_models')({ cooler, isPublic: config.public }).spreads;
|
||||
const info = await spreadsModel.forMessage(msgId);
|
||||
return info && Number(info.count) > 0 ? Number(info.count) : 0;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const renderSpreadEditWarning = async (msgId) => {
|
||||
const count = await spreadsFor(msgId);
|
||||
if (!count) return null;
|
||||
return div({ class: 'error-box spread-edit-warning' },
|
||||
p(`${i18n.spreadEditWarning} (${count})`)
|
||||
);
|
||||
};
|
||||
exports.renderSpreadEditWarning = renderSpreadEditWarning;
|
||||
|
||||
const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters) => {
|
||||
const ops = opinions || {};
|
||||
const total = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
const { form, button, div, h2, h3, p, section, input, label, br, a, span, textarea, select, option, img, strong } =
|
||||
const { form, button, div, h2, h3, p, section, input, label, br, a, span, textarea, select, option, img, strong, table, tr, td } =
|
||||
require("../server/node_modules/hyperaxe");
|
||||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, userLink, renderLifespanChip, renderSpreadButton, renderContentActions } = require("./main_views");
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning } = require("./main_views");
|
||||
const { renderEncryptedChip } = require("./clearnet_view");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderMapWithPins, renderZoomedMapWithPins, getViewportBounds, latLngToPx, pxToLatLng, MAP_W, MAP_H, getMaxTileZoom } = require("../maps/map_renderer");
|
||||
|
|
@ -20,14 +20,6 @@ const buildReturnTo = (filter, params = {}) => {
|
|||
return `/maps?${parts.join("&")}`;
|
||||
};
|
||||
|
||||
const renderPMButton = (recipient) => {
|
||||
const r = safeText(recipient);
|
||||
if (!r || String(r) === String(userId)) return null;
|
||||
return form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: r }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage));
|
||||
};
|
||||
|
||||
const renderTags = (tags) => {
|
||||
const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean);
|
||||
return list.length
|
||||
|
|
@ -35,14 +27,6 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const renderMapFavoriteToggle = (mapObj, returnTo = "") =>
|
||||
form({
|
||||
method: "POST",
|
||||
action: mapObj.isFavorite ? `/maps/favorites/remove/${encodeURIComponent(mapObj.key)}` : `/maps/favorites/add/${encodeURIComponent(mapObj.key)}`
|
||||
},
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, mapObj.isFavorite ? i18n.mapRemoveFavoriteButton : i18n.mapAddFavoriteButton));
|
||||
|
||||
let areaCounter = 0;
|
||||
|
||||
const buildAreas = (clickUrl, latParam = "lat", lngParam = "lng", viewport = null) => {
|
||||
|
|
@ -120,11 +104,14 @@ const renderMap = (markers, clickUrl, mainIdx, opts = {}) => {
|
|||
const clean = url.replace(/&/g, "&");
|
||||
return `<a href="${clean}" class="map-popup-link" target="_blank" rel="noopener">${url}</a>`;
|
||||
}).replace(/\n/g, "<br>");
|
||||
const sz = 20 * Math.pow(2, Math.max(0, zoom - getMaxTileZoom()));
|
||||
const x1 = Math.max(0, px.x - sz);
|
||||
const y1 = Math.max(0, px.y - sz);
|
||||
const x2 = Math.min(MAP_W, px.x + sz);
|
||||
const y2 = Math.min(MAP_H, px.y + sz);
|
||||
const scale = Math.pow(2, Math.max(0, zoom - getMaxTileZoom()));
|
||||
const halfW = Math.max(26, 26 * scale);
|
||||
const pinTop = Math.max(48, 48 * scale);
|
||||
const pinFoot = Math.max(10, 10 * scale);
|
||||
const x1 = Math.max(0, px.x - halfW);
|
||||
const y1 = Math.max(0, px.y - pinTop);
|
||||
const x2 = Math.min(MAP_W, px.x + halfW);
|
||||
const y2 = Math.min(MAP_H, px.y + pinFoot);
|
||||
const popupId = `${pfx}_${i}`;
|
||||
const latStr = typeof m.lat === "number" ? m.lat.toFixed(4) : "";
|
||||
const lngStr = typeof m.lng === "number" ? m.lng.toFixed(4) : "";
|
||||
|
|
@ -174,13 +161,13 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "delete-btn", type: "submit" }, i18n.mapDeleteButton))
|
||||
];
|
||||
const isPrivateMap = !mapObj.tribeId && mapObj.mapType !== "OPEN";
|
||||
if (isPrivateMap) {
|
||||
const invitable = !mapObj.tribeId && mapObj.mapType === "CLOSED";
|
||||
if (invitable) {
|
||||
actions.push(form({ method: "POST", action: `/maps/generate-invite/${encodeURIComponent(mapObj.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite)));
|
||||
}
|
||||
const openInvite = Array.isArray(mapObj.invites) ? mapObj.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code) : null;
|
||||
if (!mapObj.tribeId) {
|
||||
if (invitable) {
|
||||
if (openInvite) {
|
||||
actions.push(div({ class: 'tribe-open-invite' },
|
||||
span({ class: 'card-label' }, i18n.tribeInviteCodeText),
|
||||
|
|
@ -200,10 +187,10 @@ const renderFilters = (filter, q) =>
|
|||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterFavorites),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterRecent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterFavorites).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.mapUploadButton)));
|
||||
|
||||
const renderMapForm = (filter, mapId, mapToEdit, params = {}) => {
|
||||
|
|
@ -221,9 +208,10 @@ const renderMapForm = (filter, mapId, mapToEdit, params = {}) => {
|
|||
const cleanUrl = `/maps?filter=create${params.tribeId ? '&tribeId=' + encodeURIComponent(params.tribeId) : ''}`;
|
||||
const pickerMarkers = latVal && lngVal ? [{ lat: parseFloat(latVal), lng: parseFloat(lngVal) }] : [];
|
||||
|
||||
return div({ class: "map-create-layout" },
|
||||
div({ class: "map-form map-form-full" },
|
||||
form({
|
||||
return div({ class: "div-center audio-form" },
|
||||
params.spreadWarning || null,
|
||||
h2(filter === "edit" ? i18n.mapUpdateButton : i18n.mapCreateButton),
|
||||
form({
|
||||
action: filter === "edit" ? `/maps/update/${encodeURIComponent(mapId)}` : "/maps/create",
|
||||
method: "POST",
|
||||
enctype: "multipart/form-data"
|
||||
|
|
@ -231,40 +219,47 @@ const renderMapForm = (filter, mapId, mapToEdit, params = {}) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "filter", value: "create" }),
|
||||
params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null,
|
||||
label(i18n.title || "Title"),
|
||||
input({ type: "text", name: "title", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }),
|
||||
label(i18n.mapDescriptionLabel),
|
||||
label(i18n.title || "Title"), br(),
|
||||
input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }),
|
||||
br(), br(),
|
||||
label(i18n.mapDescriptionLabel), br(),
|
||||
textarea({ name: "description", placeholder: i18n.mapDescriptionPlaceholder, rows: "3" }, descVal),
|
||||
label(i18n.mapTagsLabel),
|
||||
br(), br(),
|
||||
label(i18n.mapTagsLabel), br(),
|
||||
input({ type: "text", name: "tags", placeholder: i18n.mapTagsPlaceholder, value: tagsValue }),
|
||||
label(i18n.mapTypeLabel),
|
||||
br(), br(),
|
||||
label(i18n.mapTypeLabel), br(),
|
||||
select({ name: "mapType" },
|
||||
option({ value: "SINGLE", ...(mapTypeVal === "SINGLE" ? { selected: true } : {}) }, "SINGLE"),
|
||||
option({ value: "OPEN", ...(mapTypeVal === "OPEN" ? { selected: true } : {}) }, "OPEN"),
|
||||
option({ value: "CLOSED", ...(mapTypeVal === "CLOSED" ? { selected: true } : {}) }, "CLOSED")),
|
||||
br(),br(),
|
||||
label(i18n.mapMarkerLabelField),
|
||||
option({ value: "SINGLE", ...(mapTypeVal === "SINGLE" ? { selected: true } : {}) }, i18n.mapTypeSingle),
|
||||
option({ value: "OPEN", ...(mapTypeVal === "OPEN" ? { selected: true } : {}) }, i18n.mapTypeOpen),
|
||||
option({ value: "CLOSED", ...(mapTypeVal === "CLOSED" ? { selected: true } : {}) }, i18n.mapTypeClosed)),
|
||||
br(), br(),
|
||||
label(i18n.mapMarkerLabelField), br(),
|
||||
textarea({ name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal),
|
||||
label(i18n.markerImageLabel || "Marker Image"),
|
||||
br(), br(),
|
||||
label(i18n.markerImageLabel || "Marker Image"), br(),
|
||||
input({ type: "file", name: "image", accept: "image/*" }),
|
||||
br(), br(),
|
||||
label(i18n.mapLatLabel),
|
||||
label(i18n.mapLatLabel), br(),
|
||||
input({ type: "text", name: "lat", placeholder: i18n.mapLatPlaceholder, value: latVal }),
|
||||
label(i18n.mapLngLabel),
|
||||
br(), br(),
|
||||
label(i18n.mapLngLabel), br(),
|
||||
input({ type: "text", name: "lng", placeholder: i18n.mapLngPlaceholder, value: lngVal }),
|
||||
br(), br(),
|
||||
div({ class: "map-form-row" },
|
||||
button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"),
|
||||
a({ href: cleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")),
|
||||
renderCoordPreview(latVal, lngVal),
|
||||
label(i18n.mapZoomLabel || "Zoom"),
|
||||
br(),
|
||||
label(i18n.mapZoomLabel || "Zoom"), br(),
|
||||
select({ name: "zoom" },
|
||||
[2, 3, 4, 5, 6, 7, 8].map(z =>
|
||||
option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))),
|
||||
br(),br(),
|
||||
br(), br(),
|
||||
button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"),
|
||||
div({ class: "map-form-map-slot" },
|
||||
renderMap(pickerMarkers, null, 0, { zoom: zoomVal, centerLat: parseFloat(latVal) || 0, centerLng: parseFloat(lngVal) || 0 })),
|
||||
button({ type: "submit", class: "create-button" }, filter === "edit" ? i18n.mapUpdateButton : i18n.mapCreateButton))));
|
||||
button({ type: "submit", class: "create-button" }, filter === "edit" ? i18n.mapUpdateButton : i18n.mapCreateButton)));
|
||||
};
|
||||
|
||||
const renderMarkerForm = (mapObj, returnTo, params = {}, tribeMembers = []) => {
|
||||
|
|
@ -341,8 +336,7 @@ const renderMarkersList = (markers, mapObj) => {
|
|||
|
||||
const renderMapCard = (mapObj, filter, params = {}) => {
|
||||
const returnTo = buildReturnTo(filter, params);
|
||||
const ownerActions = renderMapOwnerActions(filter, mapObj, params);
|
||||
const markerCount = safeArr(mapObj.markers).length;
|
||||
const markerCount = safeArr(mapObj.markers).length + 1;
|
||||
|
||||
const thumbMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat(
|
||||
safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng })));
|
||||
|
|
@ -350,41 +344,33 @@ const renderMapCard = (mapObj, filter, params = {}) => {
|
|||
const thumbSrc = thumbFile ? `/mapcache/${thumbFile}` : "/assets/images/worldmap-z2.png";
|
||||
|
||||
const chips = [
|
||||
renderStateChip("whole", "🗺", String(mapObj.mapType || "").toUpperCase()),
|
||||
renderEncryptedChip(i18n),
|
||||
renderLifespanChip(mapObj.lifetime, i18n)
|
||||
].filter(Boolean);
|
||||
|
||||
const isOwn = mapObj.author && String(mapObj.author) === String(userId);
|
||||
return div({ class: "trending-card map-card" + (isOwn ? " own-content" : "") },
|
||||
const href = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`;
|
||||
return div({ class: "tribe-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span({ class: "map-type-badge" }, mapObj.mapType),
|
||||
renderContentActions(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`)),
|
||||
div({ class: "card-section maps-card-body" },
|
||||
a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`, class: "map-card-thumb-link" },
|
||||
{ innerHTML: `<img src="${thumbSrc}" class="map-card-thumb" alt="map">` }),
|
||||
div({ class: "map-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
mapObj.title ? h2({ class: "tribe-card-title" }, a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}` }, mapObj.title)) : null
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "map-card-header" },
|
||||
div({ class: "map-card-info" },
|
||||
span({ class: "map-coords" }, `📍 ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`),
|
||||
markerCount > 0 ? span({ class: "map-marker-count" }, `▾ ${markerCount}`) : null,
|
||||
mapObj.key ? renderMapUrl(mapObj) : null),
|
||||
div({ class: "map-card-actions" },
|
||||
renderMapFavoriteToggle(mapObj, returnTo),
|
||||
renderPMButton(mapObj.author),
|
||||
...ownerActions)),
|
||||
safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null,
|
||||
(() => {
|
||||
const btn = renderSpreadButton(mapObj.key, params.spreadMap && params.spreadMap.get(mapObj.key));
|
||||
return btn ? div({ class: "card-spread-left" }, btn) : null;
|
||||
})(),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, moment(mapObj.createdAt).fromNow()),
|
||||
span(" · "),
|
||||
userLink(mapObj.author)))));
|
||||
span(),
|
||||
renderContentActions(mapObj.key, href, { spread: params.spreadMap && params.spreadMap.get(mapObj.key) || null, author: mapObj.author, favKind: 'maps', isFavorite: mapObj.isFavorite, reportTitle: mapObj.title })
|
||||
),
|
||||
div({ class: "tribe-card-image-wrapper" },
|
||||
a({ href }, img({ src: thumbSrc, class: "tribe-card-hero-image", alt: "map" }))
|
||||
),
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, a({ href }, mapObj.title || i18n.mapAllSectionTitle))
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
p({ class: "job-meta-line" }, `📍 ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`),
|
||||
safeText(mapObj.description) ? p({ class: "tribe-card-description" }, mapObj.description) : null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${markerCount}`)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderMapList = (maps, filter, params = {}) =>
|
||||
|
|
@ -392,13 +378,20 @@ const renderMapList = (maps, filter, params = {}) =>
|
|||
? maps.map((mapObj) => renderMapCard(mapObj, filter, params))
|
||||
: p(params.q ? i18n.mapNoMatch : i18n.noMaps);
|
||||
|
||||
exports.renderMapInvitePage = (code) => {
|
||||
const pageContent = div({ class: "invite-page" },
|
||||
h2(i18n.tribeInviteCodeText, code),
|
||||
form({ method: "GET", action: "/maps" },
|
||||
input({ type: "hidden", name: "filter", value: "all" }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.walletBack)
|
||||
)
|
||||
);
|
||||
return template(i18n.mapInviteMode || i18n.tribeInviteCodeText || "Invite", section(pageContent));
|
||||
};
|
||||
|
||||
exports.mapsView = async (maps, filter = "all", mapId = null, params = {}) => {
|
||||
const title = filter === "mine" ? i18n.mapMineSectionTitle
|
||||
: filter === "create" ? i18n.mapCreateSectionTitle
|
||||
: filter === "edit" ? i18n.mapUpdateSectionTitle
|
||||
: filter === "recent" ? i18n.mapRecentSectionTitle
|
||||
: filter === "favorites" ? i18n.mapFavoritesSectionTitle
|
||||
: i18n.mapAllSectionTitle;
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(mapId) };
|
||||
const title = i18n.mapTitle;
|
||||
|
||||
const q = safeText(params.q || "");
|
||||
const list = safeArr(maps);
|
||||
|
|
@ -418,7 +411,7 @@ exports.mapsView = async (maps, filter = "all", mapId = null, params = {}) => {
|
|||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.mapSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.mapSearchButton)))),
|
||||
div({ class: "maps-list" }, renderMapList(list, filter, { q })))));
|
||||
div({ class: "jobs-grid" }, renderMapList(list, filter, { q })))));
|
||||
};
|
||||
|
||||
exports.singleMapView = async (mapObj, filter = "all", params = {}) => {
|
||||
|
|
@ -426,7 +419,7 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => {
|
|||
const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q });
|
||||
const ownerActions = renderMapOwnerActions(filter, mapObj, { q });
|
||||
const tribeMembers = safeArr(params.tribeMembers);
|
||||
const zoomVal = parseInt(params.zoom) || 2;
|
||||
const zoomVal = parseInt(params.zoom) || 8;
|
||||
|
||||
const allMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat(
|
||||
safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng })));
|
||||
|
|
@ -435,55 +428,80 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => {
|
|||
safeArr(mapObj.markers).map((m) => m.label || ""));
|
||||
const pinImages = [mapObj.image || ""].concat(safeArr(mapObj.markers).map((m) => m.image || ""));
|
||||
|
||||
const mapSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(mapObj.key, null, {
|
||||
author: mapObj.author,
|
||||
favKind: 'maps',
|
||||
isFavorite: mapObj.isFavorite,
|
||||
spread: params.spreads || null,
|
||||
returnTo,
|
||||
reportTitle: mapObj.title
|
||||
})
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, mapObj.title || i18n.mapAllSectionTitle)
|
||||
),
|
||||
div({ class: "card-chips-row" },
|
||||
renderStateChip("whole", "🗺", String(mapObj.mapType || "").toUpperCase()),
|
||||
renderEncryptedChip(i18n),
|
||||
renderLifespanChip(mapObj.lifetime, i18n)
|
||||
),
|
||||
safeText(mapObj.description) ? p({ class: "tribe-side-description" }, mapObj.description) : null,
|
||||
table({ class: "tribe-info-table jobs-info-table" },
|
||||
tr(
|
||||
td({ class: "tribe-info-label" }, i18n.mapLatLabel),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, mapObj.lat.toFixed(6))
|
||||
),
|
||||
tr(
|
||||
td({ class: "tribe-info-label" }, i18n.mapLngLabel),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, mapObj.lng.toFixed(6))
|
||||
),
|
||||
tr(
|
||||
td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm"))
|
||||
),
|
||||
tr(
|
||||
td({ class: "tribe-info-value", colspan: "4" }, userLink(mapObj.author))
|
||||
)
|
||||
),
|
||||
renderTags(mapObj.tags),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${safeArr(mapObj.markers).length + 1}`)
|
||||
),
|
||||
(String(mapObj.author) !== String(userId) && !mapObj.tribeId && mapObj.mapType !== "OPEN")
|
||||
? div({ class: "tribe-side-actions" },
|
||||
a({ class: "tribe-action-btn", href: "/invites#invites-maps" }, i18n.tribeEnterInvite)
|
||||
)
|
||||
: null,
|
||||
ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null
|
||||
);
|
||||
|
||||
const mapMain = div({ class: "tribe-main" },
|
||||
renderMapUrl(mapObj),
|
||||
renderMap(allMarkers, null, 0, { pinLabels, pinImages, pinPrefix: `detail${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mapObj.lng) || 0 }),
|
||||
form({ method: "GET", action: `/maps/${encodeURIComponent(mapObj.key)}` },
|
||||
label(i18n.mapZoomLabel || "Zoom"),
|
||||
br(),
|
||||
select({ name: "zoom" },
|
||||
[2, 3, 4, 5, 6, 7, 8].map(z =>
|
||||
option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))),
|
||||
br(), br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")),
|
||||
renderMarkersList(mapObj.markers, mapObj),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(mapObj.author),
|
||||
mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt
|
||||
? span({ class: "votations-comment-date" }, ` · ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm")}`)
|
||||
: null),
|
||||
renderMarkerForm(mapObj, returnTo, params, tribeMembers)
|
||||
);
|
||||
|
||||
return template(mapObj.title || i18n.mapTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.mapTitle), p(i18n.mapDescription))),
|
||||
section(renderFilters(filter, q)),
|
||||
section(
|
||||
div({ class: "map-detail" },
|
||||
mapObj.title ? div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, mapObj.title)
|
||||
) : null,
|
||||
mapObj.title ? div({ class: "card-chips-row" },
|
||||
renderEncryptedChip(i18n),
|
||||
renderLifespanChip(mapObj.lifetime, i18n)
|
||||
) : null,
|
||||
safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null,
|
||||
div({ class: "map-detail-header" },
|
||||
div({ class: "map-detail-info" },
|
||||
span({ class: "map-type-badge" }, mapObj.mapType),
|
||||
span({ class: "map-coords-detail" }, `📍 ${mapObj.lat.toFixed(6)}, ${mapObj.lng.toFixed(6)}`)),
|
||||
div({ class: "map-detail-actions" },
|
||||
renderMapFavoriteToggle(mapObj, returnTo),
|
||||
renderPMButton(mapObj.author),
|
||||
(String(mapObj.author) !== String(userId) && !mapObj.tribeId && mapObj.mapType !== "OPEN")
|
||||
? a({ class: "tribe-action-btn", href: "/invites#invites-maps" }, i18n.tribeEnterInvite)
|
||||
: null,
|
||||
...ownerActions)),
|
||||
renderMapUrl(mapObj),
|
||||
br(),
|
||||
form({ method: "GET", action: `/maps/${encodeURIComponent(mapObj.key)}` },
|
||||
label(i18n.mapZoomLabel || "Zoom"),
|
||||
br(),
|
||||
select({ name: "zoom" },
|
||||
[2, 3, 4, 5, 6, 7, 8].map(z =>
|
||||
option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))),
|
||||
br(), br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")),
|
||||
br(),
|
||||
renderMap(allMarkers, null, 0, { pinLabels, pinImages, pinPrefix: `detail${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mapObj.lng) || 0 }),
|
||||
renderMarkersList(mapObj.markers, mapObj),
|
||||
renderTags(mapObj.tags),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(mapObj.key);
|
||||
return btn ? div({ class: "card-spread-centered" }, btn) : null;
|
||||
})(),
|
||||
br(),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
userLink(mapObj.author),
|
||||
mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt
|
||||
? span({ class: "votations-comment-date" }, ` · ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`)
|
||||
: null),
|
||||
renderMarkerForm(mapObj, returnTo, params, tribeMembers))));
|
||||
section(div({ class: "tribe-details" }, mapSide, mapMain)));
|
||||
};
|
||||
|
||||
exports.renderMapLocationUrl = (mapUrl) => {
|
||||
|
|
@ -520,6 +538,7 @@ exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => {
|
|||
const lo = parseFloat(mapData.lng) || 0;
|
||||
return div({ class: "map-embed-section" },
|
||||
span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"),
|
||||
renderMap([{ lat: la, lng: lo }], null, 0, { zoom: zoomVal, centerLat: la, centerLng: lo }),
|
||||
form({ method: "GET", action: detailUrl },
|
||||
label(i18n.mapZoomLabel || "Zoom"),
|
||||
br(),
|
||||
|
|
@ -528,8 +547,6 @@ exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => {
|
|||
option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))),
|
||||
br(), br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")),
|
||||
br(),
|
||||
renderMap([{ lat: la, lng: lo }], null, 0, { zoom: zoomVal, centerLat: la, centerLng: lo }),
|
||||
mapUrl ? div({ class: "map-embed-url" },
|
||||
a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null);
|
||||
};
|
||||
|
|
|
|||
81
src/views/mentions_view.js
Normal file
81
src/views/mentions_view.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
const { div, h2, p, section, button, form, input, span } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderContentActions } = require("./main_views");
|
||||
const { getViewDetailsAction } = require("./activity_view");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
||||
const userId = config.keys.id;
|
||||
|
||||
const excerpt = (text, max = 320) => {
|
||||
const s = String(text == null ? "" : text).trim();
|
||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
||||
};
|
||||
|
||||
const typeLabel = (type) => i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || String(type || '').toUpperCase();
|
||||
|
||||
const renderMentionCard = (item) => {
|
||||
const href = getViewDetailsAction(item.type, { id: item.id, key: item.id, author: item.author, content: item.content });
|
||||
const isOwn = String(item.author) === String(userId);
|
||||
return div({ class: "trending-card mention-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
div({ class: 'card-chips-row' },
|
||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, typeLabel(item.type))
|
||||
)
|
||||
),
|
||||
renderContentActions(item.id, href, { author: item.author, reportTitle: item.title || item.text })
|
||||
),
|
||||
div({ class: "card-section mention-card-body" },
|
||||
item.title
|
||||
? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.title))
|
||||
: null,
|
||||
item.text ? p({ class: "mention-text" }, ...renderUrl(excerpt(item.text))) : null,
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
span(" · "),
|
||||
userLink(item.author)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.mentionsView = async (items = [], filter = 'ALL', params = {}) => {
|
||||
const counts = params.counts || {};
|
||||
const types = Object.keys(counts).sort((a, b) => (counts[b] - counts[a]) || a.localeCompare(b));
|
||||
const q = params.q || '';
|
||||
|
||||
return template(
|
||||
i18n.mentions,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.mentions),
|
||||
p(i18n.mentionsDescription)
|
||||
),
|
||||
types.length
|
||||
? div({ class: "filters" },
|
||||
form({ method: "GET", action: "/mentions", class: "ui-toolbar ui-toolbar--filters" },
|
||||
...types.map(t =>
|
||||
button({ type: "submit", name: "filter", value: t, class: filter === t ? "filter-btn active" : "filter-btn" },
|
||||
`${typeLabel(t)} (${counts[t]})`)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/mentions", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.mentionsSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
items.length
|
||||
? div({ class: "mentions-list" }, ...items.map(renderMentionCard))
|
||||
: p({ class: "empty" }, i18n.noMentions)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const { form, button, div, h2, p, section, table, tr, td, input, a, br, ul, li, span, strong } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n } = require('./main_views');
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { MOBILE_MODULES, ALL_MODULES } = require('../models/workflows_model');
|
||||
|
||||
const modulesView = () => {
|
||||
const config = getConfig().modules;
|
||||
|
|
@ -19,6 +20,7 @@ const modulesView = () => {
|
|||
{ name: 'docs', label: i18n.modulesDocsLabel, description: i18n.modulesDocsDescription },
|
||||
{ name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription },
|
||||
{ name: 'favorites', label: i18n.modulesFavoritesLabel, description: i18n.modulesFavoritesDescription },
|
||||
{ name: 'blogs', label: i18n.modulesBlogsLabel, description: i18n.modulesBlogsDescription },
|
||||
{ name: 'fediverse', label: i18n.modulesFediverseLabel, description: i18n.modulesFediverseDescription },
|
||||
{ name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription },
|
||||
{ name: 'forum', label: i18n.modulesForumLabel, description: i18n.modulesForumDescription },
|
||||
|
|
@ -29,26 +31,21 @@ const modulesView = () => {
|
|||
{ name: 'invites', label: i18n.modulesInvitesLabel, description: i18n.modulesInvitesDescription },
|
||||
{ name: 'jobs', label: i18n.modulesJobsLabel, description: i18n.modulesJobsDescription },
|
||||
{ name: 'larp', label: i18n.modulesLarpLabel, description: i18n.modulesLarpDescription },
|
||||
{ name: 'latest', label: i18n.modulesLatestLabel, description: i18n.modulesLatestDescription },
|
||||
{ name: 'legacy', label: i18n.modulesLegacyLabel, description: i18n.modulesLegacyDescription },
|
||||
{ name: 'logs', label: i18n.modulesLogsLabel, description: i18n.modulesLogsDescription },
|
||||
{ name: 'maps', label: i18n.modulesMapLabel, description: i18n.modulesMapDescription },
|
||||
{ name: 'market', label: i18n.modulesMarketLabel, description: i18n.modulesMarketDescription },
|
||||
{ name: 'melody', label: i18n.modulesMelodyLabel, description: i18n.modulesMelodyDescription },
|
||||
{ name: 'multiverse', label: i18n.modulesMultiverseLabel, description: i18n.modulesMultiverseDescription },
|
||||
{ name: 'opinions', label: i18n.modulesOpinionsLabel, description: i18n.modulesOpinionsDescription },
|
||||
{ name: 'pads', label: i18n.modulesPadsLabel, description: i18n.modulesPadsDescription },
|
||||
{ name: 'parliament', label: i18n.modulesParliamentLabel, description: i18n.modulesParliamentDescription },
|
||||
{ name: 'pixelia', label: i18n.modulesPixeliaLabel, description: i18n.modulesPixeliaDescription },
|
||||
{ name: 'popular', label: i18n.modulesPopularLabel, description: i18n.modulesPopularDescription },
|
||||
{ name: 'polls', label: i18n.modulesPollsLabel, description: i18n.modulesPollsDescription },
|
||||
{ name: 'projects', label: i18n.modulesProjectsLabel, description: i18n.modulesProjectsDescription },
|
||||
{ name: 'reports', label: i18n.modulesReportsLabel, description: i18n.modulesReportsDescription },
|
||||
{ name: 'shops', label: i18n.modulesShopsLabel, description: i18n.modulesShopsDescription },
|
||||
{ name: 'summaries', label: i18n.modulesSummariesLabel, description: i18n.modulesSummariesDescription },
|
||||
{ name: 'tags', label: i18n.modulesTagsLabel, description: i18n.modulesTagsDescription },
|
||||
{ name: 'tasks', label: i18n.modulesTasksLabel, description: i18n.modulesTasksDescription },
|
||||
{ name: 'threads', label: i18n.modulesThreadsLabel, description: i18n.modulesThreadsDescription },
|
||||
{ name: 'topics', label: i18n.modulesTopicsLabel, description: i18n.modulesTopicsDescription },
|
||||
{ name: 'torrents', label: i18n.modulesTorrentsLabel, description: i18n.modulesTorrentsDescription },
|
||||
{ name: 'transfers', label: i18n.modulesTransfersLabel, description: i18n.modulesTransfersDescription },
|
||||
{ name: 'trending', label: i18n.modulesTrendingLabel, description: i18n.modulesTrendingDescription },
|
||||
|
|
@ -70,34 +67,35 @@ const modulesView = () => {
|
|||
const header = div({ class: 'tags-header' },
|
||||
h2(i18n.modulesViewTitle),
|
||||
p(i18n.modulesViewDescription),
|
||||
div({ style: 'margin-bottom:16px;' },
|
||||
div({ class: 'modules-counts' },
|
||||
`${i18n.modulesTotalModulesLabel}: `,
|
||||
strong({ style: 'color:#888;' }, totalModulesCount),
|
||||
ul({ style: 'list-style-type:none; padding:0; margin:0;' },
|
||||
li({ style: 'font-size:18px; color:#555; margin:8px 0;' },
|
||||
strong({ class: 'modules-count-value' }, totalModulesCount),
|
||||
ul({ class: 'modules-count-list' },
|
||||
li({ class: 'modules-count-item' },
|
||||
`${i18n.modulesEnabledModulesLabel}: `,
|
||||
span({ style: 'color:#888;' }, activeModulesCount)
|
||||
span({ class: 'modules-count-value' }, activeModulesCount)
|
||||
),
|
||||
li({ style: 'font-size:18px; color:#555; margin:8px 0;' },
|
||||
li({ class: 'modules-count-item' },
|
||||
`${i18n.modulesDisabledModulesLabel}: `,
|
||||
span({ style: 'color:#888;' }, disabledModulesCount)
|
||||
span({ class: 'modules-count-value' }, disabledModulesCount)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const PRESETS = {
|
||||
minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'popular', 'latest', 'threads', 'opinions', 'cipher', 'legacy'],
|
||||
social: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes'],
|
||||
economy: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'jobs', 'shops', 'industry'],
|
||||
full: modules.map(m => m.name)
|
||||
minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'blogs', 'polls', 'opinions', 'cipher', 'legacy'],
|
||||
social: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes'],
|
||||
economy: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'jobs', 'shops', 'industry'],
|
||||
mobile: MOBILE_MODULES,
|
||||
full: ALL_MODULES
|
||||
};
|
||||
|
||||
const presetButtons = div({ class: 'preset-group', style: 'display:flex;gap:8px;flex-wrap:nowrap;margin-bottom:16px;' },
|
||||
const presetButtons = div({ class: 'preset-group' },
|
||||
Object.entries(PRESETS).map(([key, mods]) => {
|
||||
const presetLabel = (i18n[`modulesPreset_${key}`] || key).toUpperCase();
|
||||
const isActive = modules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on'));
|
||||
return form({ action: "/modules/preset", method: "post", style: "display:inline;margin:0;" },
|
||||
return form({ action: "/modules/preset", method: "post", class: "modules-preset-form" },
|
||||
input({ type: "hidden", name: "preset", value: key }),
|
||||
button({
|
||||
type: 'submit',
|
||||
|
|
@ -120,7 +118,7 @@ const modulesView = () => {
|
|||
tr(
|
||||
td(i18n.modulesModuleName),
|
||||
td(i18n.modulesModuleDescription),
|
||||
td({ style: 'text-align: center;' }, i18n.modulesModuleStatus)
|
||||
td({ class: 'module-status-cell' }, i18n.modulesModuleStatus)
|
||||
),
|
||||
modules.map(mod =>
|
||||
tr(
|
||||
|
|
@ -140,7 +138,7 @@ const modulesView = () => {
|
|||
)
|
||||
),
|
||||
div(
|
||||
{ class: "save-button-container", style: "margin-top: 20px; text-align: center;" },
|
||||
{ class: "save-button-container" },
|
||||
button({ type: "submit", class: "submit-button" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -102,10 +102,6 @@ const renderContentHtml = (content, key) => {
|
|||
p(...renderUrl(content.description))
|
||||
]
|
||||
: null,
|
||||
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' })
|
||||
|
|
@ -265,7 +261,7 @@ const renderContentHtml = (content, key) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
||||
exports.opinionsView = (items, filter, spreadMap = new Map(), q = '') => {
|
||||
seenDocumentTitles.clear();
|
||||
items = items
|
||||
.filter(item => {
|
||||
|
|
@ -282,7 +278,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
});
|
||||
|
||||
const title = i18n.opinionsTitle;
|
||||
const baseFilters = ['TOP', 'ALL', 'MINE', 'RECENT'];
|
||||
const baseFilters = ['ALL', 'MINE', 'RECENT', 'TOP'];
|
||||
|
||||
const cards = items
|
||||
.map(item => {
|
||||
|
|
@ -303,12 +299,11 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase())
|
||||
),
|
||||
renderContentActions(key, detailHref(c.type, key))
|
||||
renderContentActions(key, detailHref(c.type, key), { spread: spreadMap.get(key) || null })
|
||||
),
|
||||
div(
|
||||
{ class: 'card-section opinions-card-body' },
|
||||
contentHtml,
|
||||
div({ class: 'card-spread-left' }, renderSpreadButton(key, spreadMap.get(key))),
|
||||
p({ class: 'card-footer' },
|
||||
span({ class: 'date-link' }, `${created} ${i18n.performed} `),
|
||||
userLink(item.value.author)
|
||||
|
|
@ -331,10 +326,12 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
);
|
||||
})(),
|
||||
details({ class: 'opinions-voting-collapse' },
|
||||
summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`),
|
||||
summary({ class: 'opinions-summary' },
|
||||
span({ class: 'opinions-summary-icon' }, 'ꔍ'),
|
||||
span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)),
|
||||
div({ class: 'voting-buttons' },
|
||||
allCats.map(cat => {
|
||||
const label = `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${c.opinions?.[cat] || 0}]`;
|
||||
const label = `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${c.opinions?.[cat] || 0}]`;
|
||||
if (voted) {
|
||||
return button({ class: 'vote-btn', type: 'button' }, label);
|
||||
}
|
||||
|
|
@ -364,7 +361,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
baseFilters.map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -372,7 +369,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.positive.slice(0, 5).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -380,7 +377,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.positive.slice(5, 10).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -388,7 +385,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.positive.slice(10, 15).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -398,7 +395,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.constructive.slice(0, 5).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -406,7 +403,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.constructive.slice(5, 11).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -414,7 +411,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.moderation.slice(0, 5).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -422,11 +419,20 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => {
|
|||
opinionCategories.moderation.slice(5, 10).map(mode =>
|
||||
form({ method: 'GET', action: '/opinions' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode)
|
||||
button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase())
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/opinions', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({ type: 'text', name: 'q', value: q, placeholder: i18n.opinionsSearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
cards.length
|
||||
? div({ class: 'opinions-container' }, ...cards)
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => {
|
|||
h2(i18n.parliamentElectionsStatusTitle),
|
||||
div({ class: 'card-field card-field--spaced' },
|
||||
span({ class: 'card-label' }, winLbl + ': '),
|
||||
span({ class: 'card-value' }, hasQuorum ? idLink : span({ style: 'color:#ffcc00;font-weight:bold;' }, i18n.voteNoQuorum || 'NO QUORUM'))
|
||||
span({ class: 'card-value' }, hasQuorum ? idLink : span({ class: 'vote-no-quorum' }, i18n.voteNoQuorum || 'NO QUORUM'))
|
||||
),
|
||||
hasQuorum
|
||||
? div({ class: 'card-field card-field--spaced' },
|
||||
|
|
@ -344,7 +344,7 @@ const ProposalForm = () =>
|
|||
form(
|
||||
{ method: 'POST', action: '/parliament/proposals/create' },
|
||||
label(i18n.parliamentProposalTitle), br(),
|
||||
input({ type: 'text', name: 'title', required: true }), br(), br(),
|
||||
input({ type: 'text', name: 'title', maxlength: '100', required: true }), br(), br(),
|
||||
label(i18n.parliamentProposalDescription), br(),
|
||||
textarea({ name: 'description', rows: 5, maxlength: 1000 }), br(), br(),
|
||||
button({ type: 'submit', class: 'create-button' }, i18n.parliamentProposalPublish)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, ul, li, table, thead, tbody, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views")
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning } = require("./main_views")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
|
|
@ -308,8 +309,8 @@ const renderMilestonesAndBounties = (project, filter, editable) => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "milestoneIndex" },
|
||||
option({ value: "", selected: b.milestoneIndex == null }, "-"),
|
||||
...milestones.map((m2, idx2) => option({ value: String(idx2), selected: b.milestoneIndex === idx2 }, m2.title))
|
||||
option({ value: "", ...(b.milestoneIndex == null ? { selected: true } : {})}, "-"),
|
||||
...milestones.map((m2, idx2) => option({ value: String(idx2), ...(b.milestoneIndex === idx2 ? { selected: true } : {})}, m2.title))
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
|
|
@ -408,7 +409,7 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`)
|
||||
renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: spreadMap.get(pr.id || pr.key) || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title })
|
||||
),
|
||||
div({ class: "card-section project-card-body" },
|
||||
heroNode,
|
||||
|
|
@ -426,15 +427,14 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM
|
|||
: null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.projectFollowers}: ${followersCount(pr)}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(pr.id || pr.key, spreadMap.get(pr.id || pr.key)))
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const renderProjectForm = (project, mode) => {
|
||||
const renderProjectForm = (project, mode, spreadWarning = null) => {
|
||||
const pr = project || {}
|
||||
const isEdit = mode === "edit"
|
||||
const nowLocal = moment().format("YYYY-MM-DDTHH:mm")
|
||||
|
|
@ -444,6 +444,7 @@ const renderProjectForm = (project, mode) => {
|
|||
|
||||
return div(
|
||||
{ class: "div-center project-form" },
|
||||
isEdit ? spreadWarning : null,
|
||||
form(
|
||||
{
|
||||
action: isEdit ? `/projects/update/${encodeURIComponent(pr.id)}` : "/projects/create",
|
||||
|
|
@ -453,7 +454,7 @@ const renderProjectForm = (project, mode) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
label(i18n.projectTitle),
|
||||
br(),
|
||||
input({ type: "text", name: "title", required: true, placeholder: i18n.projectTitlePlaceholder, value: pr.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.projectTitlePlaceholder, value: pr.title || "" }),
|
||||
br(),
|
||||
label(i18n.projectDescription),
|
||||
br(),
|
||||
|
|
@ -505,6 +506,10 @@ const renderProjectForm = (project, mode) => {
|
|||
}
|
||||
|
||||
exports.projectsView = async (projectsOrForm, filter, _unused, params = {}) => {
|
||||
if (String(filter).toUpperCase() === "EDIT") {
|
||||
const editing = Array.isArray(projectsOrForm) ? projectsOrForm[0] : projectsOrForm
|
||||
params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && (editing.id || editing.key)) }
|
||||
}
|
||||
const f = String(filter || "ALL").toUpperCase()
|
||||
const filterObj = FILTERS.find((x) => x.key === f) || FILTERS[0]
|
||||
const sectionTitle = i18n[filterObj.title] || i18n.projectAllTitle
|
||||
|
|
@ -524,14 +529,25 @@ exports.projectsView = async (projectsOrForm, filter, _unused, params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/projects", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n]))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n]).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.projectCreateProject))
|
||||
)
|
||||
),
|
||||
f === "CREATE" || f === "EDIT"
|
||||
? null
|
||||
: div({ class: "filters" },
|
||||
form({ method: "GET", action: "/projects", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: f }),
|
||||
input({ type: "text", name: "q", value: safeText(params.q), placeholder: i18n.projectSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
f === "CREATE" || f === "EDIT"
|
||||
? (() => {
|
||||
const prToEdit = f === "EDIT" ? (safeArr(projectsOrForm)[0] || {}) : (params.prefill || {})
|
||||
return renderProjectForm(prToEdit, f === "EDIT" ? "edit" : "create")
|
||||
return renderProjectForm(prToEdit, f === "EDIT" ? "edit" : "create", params.spreadWarning)
|
||||
})()
|
||||
: (f === "BACKERS"
|
||||
? renderBackersLeaderboard(projectsOrForm)
|
||||
|
|
@ -564,12 +580,6 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => {
|
|||
].filter(Boolean)
|
||||
|
||||
const sideActions = []
|
||||
if (!isAuthor && pr.author) {
|
||||
sideActions.push(form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: pr.author }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
))
|
||||
}
|
||||
if (!isAuthor && statusUpper === "ACTIVE") {
|
||||
sideActions.push(isFollower
|
||||
? form({ method: "POST", action: `/projects/unfollow/${encodeURIComponent(pr.id)}` },
|
||||
|
|
@ -584,7 +594,7 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => {
|
|||
}
|
||||
if (isAuthor) {
|
||||
sideActions.push(form({ method: "GET", action: `/projects/edit/${encodeURIComponent(pr.id)}` },
|
||||
button({ class: "update-btn", type: "submit" }, i18n.projectUpdateButton)
|
||||
button({ class: "tribe-action-btn", type: "submit" }, i18n.projectUpdateButton)
|
||||
))
|
||||
sideActions.push(form({ method: "POST", action: `/projects/delete/${encodeURIComponent(pr.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
|
|
@ -595,10 +605,10 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
select(
|
||||
{ name: "status", class: "project-control-select" },
|
||||
option({ value: "ACTIVE", selected: statusUpper === "ACTIVE" }, i18n.projectStatusACTIVE),
|
||||
option({ value: "PAUSED", selected: statusUpper === "PAUSED" }, i18n.projectStatusPAUSED),
|
||||
option({ value: "COMPLETED", selected: statusUpper === "COMPLETED" }, i18n.projectStatusCOMPLETED),
|
||||
option({ value: "CANCELLED", selected: statusUpper === "CANCELLED" }, i18n.projectStatusCANCELLED)
|
||||
option({ value: "ACTIVE", ...(statusUpper === "ACTIVE" ? { selected: true } : {})}, i18n.projectStatusACTIVE),
|
||||
option({ value: "PAUSED", ...(statusUpper === "PAUSED" ? { selected: true } : {})}, i18n.projectStatusPAUSED),
|
||||
option({ value: "COMPLETED", ...(statusUpper === "COMPLETED" ? { selected: true } : {})}, i18n.projectStatusCOMPLETED),
|
||||
option({ value: "CANCELLED", ...(statusUpper === "CANCELLED" ? { selected: true } : {})}, i18n.projectStatusCANCELLED)
|
||||
),
|
||||
button({ class: "status-btn project-control-btn", type: "submit" }, i18n.projectSetStatus)
|
||||
))
|
||||
|
|
@ -611,11 +621,13 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => {
|
|||
}
|
||||
|
||||
const projectSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(pr.id || pr.key, null, { spread: params.spreads || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, pr.title)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(pr.id || pr.key, params.spreads)),
|
||||
pr.image ? renderMediaBlob(pr.image, { class: "tribe-detail-image" }) : null,
|
||||
div({ class: "job-price-line card-salary" }, `${pr.goal || 0} ECO`),
|
||||
div({ class: "job-price-line card-salary" }, `${i18n.projectFollowers}: ${followersCount(pr)}`),
|
||||
|
|
@ -642,41 +654,14 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => {
|
|||
span({ class: "date-link" }, `${moment(pr.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(pr.author)
|
||||
),
|
||||
renderOpinionsVoting('/projects/opinions', pr.id || pr.key, pr.opinions, null, pr.opinions_inhabitants),
|
||||
div(
|
||||
{ id: "comments", class: "vote-comments-section" },
|
||||
div({ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), br(), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
(() => {
|
||||
const visibleComments = (comments || []).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text
|
||||
return t && String(t).trim()
|
||||
})
|
||||
return visibleComments.length
|
||||
? div({ class: "comments-list" },
|
||||
visibleComments.map((c) => {
|
||||
const author = c?.value?.author || ""
|
||||
const ts = c?.value?.timestamp || c?.timestamp
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""
|
||||
const relDate = ts ? moment(ts).fromNow() : ""
|
||||
return div({ class: "comment-card" },
|
||||
div({ class: "comment-header" }, userLink(author)),
|
||||
div({ class: "comment-date" }, span({ title: absDate }, relDate)),
|
||||
div({ class: "comment-body" }, ...renderUrl(c?.value?.content?.text || ""))
|
||||
)
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
})()
|
||||
)
|
||||
renderEngagement(pr.id || pr.key,
|
||||
renderOpinionsVoting('/projects/opinions', pr.id || pr.key, pr.opinions, null, pr.opinions_inhabitants),
|
||||
renderSharedCommentsSection({
|
||||
action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`,
|
||||
comments,
|
||||
returnTo: `/projects/${encodeURIComponent(pr.id || pr.key)}`
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
return template(
|
||||
|
|
@ -687,7 +672,7 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/projects", class: "ui-toolbar ui-toolbar--filters" },
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n]))
|
||||
FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n]).toUpperCase()))
|
||||
.concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.projectCreateProject))
|
||||
)
|
||||
),
|
||||
|
|
@ -720,6 +705,7 @@ exports.clearnetProjectView = async (project) => {
|
|||
.cn-prj-funding-amount{color:var(--fg);font-size:18px;font-weight:700;margin-bottom:8px}
|
||||
.cn-prj-bar{height:8px;background:#000;border-radius:4px;overflow:hidden}
|
||||
.cn-prj-bar-fill{height:100%;background:var(--fg);border-radius:4px}
|
||||
${Array.from({ length: 21 }, (_, i) => `.cn-prj-bar-fill-${i * 5}{width:${i * 5}%}`).join('\n')}
|
||||
.cn-prj-section{margin-top:24px}
|
||||
.cn-prj-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}
|
||||
.cn-prj-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px;margin:0}
|
||||
|
|
@ -741,7 +727,7 @@ exports.clearnetProjectView = async (project) => {
|
|||
<div class="cn-prj-funding">
|
||||
<div class="cn-prj-funding-label">Funding</div>
|
||||
<div class="cn-prj-funding-amount">${pledged.toFixed(2)} / ${goal.toFixed(2)} ECO · ${fundingPct}%</div>
|
||||
<div class="cn-prj-bar"><div class="cn-prj-bar-fill" style="width:${fundingPct}%"></div></div>
|
||||
<div class="cn-prj-bar"><div class="cn-prj-bar-fill cn-prj-bar-fill-${Math.round(fundingPct / 5) * 5}"></div></div>
|
||||
${deadline ? `<div class="cn-prj-deadline">Deadline: ${deadline}</div>` : ''}
|
||||
</div>` : ''}
|
||||
${desc ? `<div class="cn-prj-section"><h2>Description</h2><p>${desc}</p></div>` : ''}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
const { div, h2, p, section, button, form, a, textarea, br, input, img, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions } = require("./main_views");
|
||||
const { renderPhotoGallery, renderGalleryFields } = require("./gallery_view");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
|
@ -39,7 +41,7 @@ const STATUS_BY_FILTER = {
|
|||
};
|
||||
|
||||
const opt = (value, isSelected, text) =>
|
||||
option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text);
|
||||
option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text);
|
||||
|
||||
const hasAnyTemplateValue = (t) => {
|
||||
if (!t || typeof t !== "object") return false;
|
||||
|
|
@ -62,15 +64,6 @@ const renderStackedTextField = (lbl, val) =>
|
|||
)
|
||||
: null;
|
||||
|
||||
const renderPmButton = (recipientId) =>
|
||||
recipientId && String(recipientId) !== String(userId)
|
||||
? form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: recipientId }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
)
|
||||
: null;
|
||||
|
||||
const renderReportStatusSetter = (report) => {
|
||||
const st = normalizeStatus(report && report.status ? report.status : "OPEN");
|
||||
return form(
|
||||
|
|
@ -135,7 +128,6 @@ const renderTemplateDetails = (report) => {
|
|||
h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle),
|
||||
renderStackedTextField(i18n.reportsContentLocationLabel + ":", t.contentLocation),
|
||||
renderStackedTextField(i18n.reportsWhyInappropriateLabel + ":", t.whyInappropriate),
|
||||
renderStackedTextField(i18n.reportsRequestedActionLabel + ":", t.requestedAction),
|
||||
renderStackedTextField(i18n.reportsEvidenceLinksLabel + ":", t.evidenceLinks)
|
||||
);
|
||||
}
|
||||
|
|
@ -144,73 +136,11 @@ const renderTemplateDetails = (report) => {
|
|||
};
|
||||
|
||||
const renderReportCommentsSection = (reportId, comments = []) => {
|
||||
const commentsCount = Array.isArray(comments) ? comments.length : 0;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{
|
||||
method: "POST",
|
||||
action: `/reports/${encodeURIComponent(reportId)}/comments`,
|
||||
class: "comment-form",
|
||||
enctype: "multipart/form-data"
|
||||
},
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
(() => {
|
||||
const visibleComments = (comments || []).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
});
|
||||
return visibleComments.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
visibleComments.map((c) => {
|
||||
const author = c.value && c.value.author ? c.value.author : "";
|
||||
const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
|
||||
const content = c.value && c.value.content ? c.value.content : {};
|
||||
const root = content.fork || content.root || "";
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet);
|
||||
})()
|
||||
);
|
||||
return renderSharedCommentsSection({
|
||||
action: `/reports/${encodeURIComponent(reportId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: null
|
||||
});
|
||||
};
|
||||
|
||||
const renderTemplateForCategory = (category, templateData = {}) => {
|
||||
|
|
@ -257,26 +187,6 @@ const renderTemplateForCategory = (category, templateData = {}) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (cat === "ABUSE") {
|
||||
return div(
|
||||
{ class: "report-template-block" },
|
||||
h2({ class: "report-template-title" }, i18n.reportsAbuseTemplateTitle),
|
||||
label(i18n.reportsWhatHappenedLabel),
|
||||
br(),
|
||||
textarea({ name: "whatHappened", rows: "4", placeholder: i18n.reportsWhatHappenedPlaceholder }, tval("whatHappened")),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsReportedUserLabel),
|
||||
br(),
|
||||
textarea({ name: "reportedUser", rows: "2", placeholder: i18n.reportsReportedUserPlaceholder }, tval("reportedUser")),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsEvidenceLinksLabel),
|
||||
br(),
|
||||
textarea({ name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks"))
|
||||
);
|
||||
}
|
||||
|
||||
if (cat === "CONTENT") {
|
||||
return div(
|
||||
{ class: "report-template-block" },
|
||||
|
|
@ -291,11 +201,6 @@ const renderTemplateForCategory = (category, templateData = {}) => {
|
|||
textarea({ name: "whyInappropriate", rows: "4", placeholder: i18n.reportsWhyInappropriatePlaceholder }, tval("whyInappropriate")),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsRequestedActionLabel),
|
||||
br(),
|
||||
textarea({ name: "requestedAction", rows: "3", placeholder: i18n.reportsRequestedActionPlaceholder }, tval("requestedAction")),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsEvidenceLinksLabel),
|
||||
br(),
|
||||
textarea({ name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks"))
|
||||
|
|
@ -377,6 +282,10 @@ const renderReportCard = (report, userId, currentFilter = "all", spreadInfo) =>
|
|||
].filter(Boolean);
|
||||
|
||||
return div({ class: "tribe-card report-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: spreadInfo || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false })
|
||||
),
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" },
|
||||
|
|
@ -386,33 +295,13 @@ const renderReportCard = (report, userId, currentFilter = "all", spreadInfo) =>
|
|||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(report.id, spreadInfo)),
|
||||
div({ class: "card-visit-btn-centered" },
|
||||
form({ method: "GET", action: `/reports/${encodeURIComponent(report.id)}` },
|
||||
input({ type: "hidden", name: "filter", value: currentFilter }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.viewReport || "View Report")
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.reportView = async (reports, filter, reportId, createCategory, params = {}) => {
|
||||
const title =
|
||||
filter === "create" ? i18n.reportsCreateButton :
|
||||
filter === "edit" ? i18n.reportsUpdateButton :
|
||||
filter === "mine" ? i18n.reportsMineSectionTitle :
|
||||
filter === "features" ? i18n.reportsFeaturesSectionTitle :
|
||||
filter === "bugs" ? i18n.reportsBugsSectionTitle :
|
||||
filter === "abuse" ? i18n.reportsAbuseSectionTitle :
|
||||
filter === "content" ? i18n.reportsContentSectionTitle :
|
||||
filter === "confirmed" ? i18n.reportsConfirmedSectionTitle :
|
||||
filter === "open" ? i18n.reportsOpenSectionTitle :
|
||||
filter === "under_review" ? i18n.reportsUnderReviewSectionTitle :
|
||||
filter === "resolved" ? i18n.reportsResolvedSectionTitle :
|
||||
filter === "invalid" ? i18n.reportsInvalidSectionTitle :
|
||||
i18n.reportsAllSectionTitle;
|
||||
const title = i18n.reportsTitle;
|
||||
|
||||
let filtered = Array.isArray(reports) ? reports : [];
|
||||
|
||||
|
|
@ -433,18 +322,20 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
const reportToEdit = filter === "edit"
|
||||
? (Array.isArray(reports) ? reports.find((r) => r.id === reportId) : null)
|
||||
: null;
|
||||
const formData = reportToEdit || params.draft || {};
|
||||
|
||||
const btnClass = (v) => (filter === v ? "filter-btn active" : "filter-btn");
|
||||
|
||||
const selectedCategory = normU(
|
||||
const rawCategory = normU(
|
||||
filter === "create"
|
||||
? (createCategory || "FEATURES")
|
||||
: (reportToEdit?.category || "FEATURES")
|
||||
);
|
||||
const selectedCategory = ["FEATURES", "BUGS", "CONTENT"].includes(rawCategory) ? rawCategory : "FEATURES";
|
||||
|
||||
const selectedTemplate = reportToEdit?.template && typeof reportToEdit.template === "object" ? reportToEdit.template : {};
|
||||
const applyLabel = i18n.apply || "Apply";
|
||||
const sev = String(reportToEdit?.severity || "low");
|
||||
const sev = String(formData.severity || "low");
|
||||
const hiddenDescription = String(reportToEdit?.description || "");
|
||||
|
||||
return template(
|
||||
|
|
@ -458,31 +349,51 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
div(
|
||||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/reports" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, i18n.reportsFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, i18n.reportsFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, i18n.reportsFilterFeatures),
|
||||
button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, i18n.reportsFilterBugs),
|
||||
button({ type: "submit", name: "filter", value: "abuse", class: btnClass("abuse") }, i18n.reportsFilterAbuse),
|
||||
button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, i18n.reportsFilterContent),
|
||||
button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, i18n.reportsFilterConfirmed),
|
||||
button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, i18n.reportsFilterOpen),
|
||||
button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, i18n.reportsFilterUnderReview),
|
||||
button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, i18n.reportsFilterResolved),
|
||||
button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, i18n.reportsFilterInvalid),
|
||||
{ method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, String(i18n.reportsFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, String(i18n.reportsFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: btnClass("recent") }, String(i18n.reportsFilterRecent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "top", class: btnClass("top") }, String(i18n.reportsFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, String(i18n.reportsFilterFeatures).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, String(i18n.reportsFilterBugs).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, String(i18n.reportsFilterContent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, String(i18n.reportsFilterConfirmed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton)
|
||||
),
|
||||
form(
|
||||
{ method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" },
|
||||
button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, String(i18n.reportsFilterOpen).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, String(i18n.reportsFilterUnderReview).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, String(i18n.reportsFilterResolved).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, String(i18n.reportsFilterInvalid).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_low", class: btnClass("sev_low") }, String(i18n.reportsSeverityLow).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_medium", class: btnClass("sev_medium") }, String(i18n.reportsSeverityMedium).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_high", class: btnClass("sev_high") }, String(i18n.reportsSeverityHigh).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_critical", class: btnClass("sev_critical") }, String(i18n.reportsSeverityCritical).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
filter === "edit" || filter === "create"
|
||||
? null
|
||||
: div({ class: "filters" },
|
||||
form({ method: "GET", action: "/reports", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.reportsSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
filter === "edit" || filter === "create"
|
||||
? div(
|
||||
{ class: "report-form" },
|
||||
filter === "edit" ? await renderSpreadEditWarning(reportToEdit && (reportToEdit.id || reportToEdit.key)) : null,
|
||||
filter === "create"
|
||||
? div(
|
||||
label(i18n.reportsTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", required: true, value: params.prefillTitle || "", form: "report-create-form" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, value: params.prefillTitle || "", form: "report-create-form" }),
|
||||
br(),
|
||||
br(),
|
||||
form(
|
||||
|
|
@ -491,10 +402,9 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
label(i18n.reportsCategory),
|
||||
br(),
|
||||
select(
|
||||
{ name: "category" },
|
||||
{ name: "category", class: "report-category-select" },
|
||||
opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures),
|
||||
opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs),
|
||||
opt("ABUSE", selectedCategory === "ABUSE", i18n.reportsCategoryAbuse),
|
||||
opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent)
|
||||
),
|
||||
br(),
|
||||
|
|
@ -508,6 +418,15 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
{ id: "report-create-form", action: "/reports/create", method: "POST", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "category", value: selectedCategory }),
|
||||
input({ type: "hidden", name: "description", value: "" }),
|
||||
h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle),
|
||||
renderTemplateForCategory(selectedCategory, params.draft || {}),
|
||||
...renderGalleryFields(formData, false, 8),
|
||||
br(),
|
||||
label("Tags"),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", value: Array.isArray(formData.tags) ? formData.tags.join(", ") : (formData.tags || "") }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsSeverity),
|
||||
br(),
|
||||
select(
|
||||
|
|
@ -519,18 +438,6 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
),
|
||||
br(),
|
||||
br(),
|
||||
h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle),
|
||||
renderTemplateForCategory(selectedCategory, {}),
|
||||
label(i18n.reportsUploadFile),
|
||||
br(),
|
||||
input({ type: "file", name: "image" }),
|
||||
br(),
|
||||
br(),
|
||||
label("Tags"),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", value: "" }),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit", class: "create-button" }, i18n.reportsCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -539,7 +446,7 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
{ id: "report-edit-form", action: `/reports/update/${encodeURIComponent(reportId)}`, method: "POST", enctype: "multipart/form-data" },
|
||||
label(i18n.reportsTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", required: true, value: reportToEdit?.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, value: reportToEdit?.title || "" }),
|
||||
br(),
|
||||
br(),
|
||||
input({ type: "hidden", name: "description", value: hiddenDescription }),
|
||||
|
|
@ -549,11 +456,21 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
{ name: "category", required: true },
|
||||
opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures),
|
||||
opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs),
|
||||
opt("ABUSE", selectedCategory === "ABUSE", i18n.reportsCategoryAbuse),
|
||||
opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle),
|
||||
renderTemplateForCategory(selectedCategory, selectedTemplate),
|
||||
br(),
|
||||
br(),
|
||||
...renderGalleryFields(reportToEdit || {}, true, 8),
|
||||
br(),
|
||||
label("Tags"),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", value: reportToEdit?.tags?.join(", ") || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsSeverity),
|
||||
br(),
|
||||
select(
|
||||
|
|
@ -565,20 +482,6 @@ exports.reportView = async (reports, filter, reportId, createCategory, params =
|
|||
),
|
||||
br(),
|
||||
br(),
|
||||
h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle),
|
||||
renderTemplateForCategory(selectedCategory, selectedTemplate),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.reportsUploadFile),
|
||||
br(),
|
||||
input({ type: "file", name: "image" }),
|
||||
br(),
|
||||
br(),
|
||||
label("Tags"),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", value: reportToEdit?.tags?.join(", ") || "" }),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.reportsUpdateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -605,21 +508,16 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) =>
|
|||
].filter(Boolean);
|
||||
|
||||
const sideActions = [];
|
||||
const pm = renderPmButton(report.author);
|
||||
if (pm) sideActions.push(pm);
|
||||
sideActions.push(form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.reportsConfirmButton)
|
||||
));
|
||||
sideActions.push(a({ href: "/tasks?filter=create", target: "_blank" },
|
||||
button({ type: "button", class: "filter-btn" }, i18n.reportsCreateTaskButton)
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsConfirmButton)
|
||||
));
|
||||
const ownerActions = [];
|
||||
if (isAuthor) {
|
||||
sideActions.push(renderReportStatusSetter(report));
|
||||
sideActions.push(form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` },
|
||||
button({ type: "submit", class: "update-btn" }, i18n.reportsUpdateButton)
|
||||
ownerActions.push(form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsUpdateButton)
|
||||
));
|
||||
sideActions.push(form({ method: "POST", action: `/reports/delete/${encodeURIComponent(report.id)}` },
|
||||
button({ type: "submit", class: "delete-btn" }, i18n.reportsDeleteButton)
|
||||
ownerActions.push(form({ method: "POST", action: `/reports/delete/${encodeURIComponent(report.id)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.reportsDeleteButton)
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -640,53 +538,72 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) =>
|
|||
pushRow(i18n.reportsCategory, report.category);
|
||||
|
||||
const reportSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(report.id, null, { spread: params.spreads || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, report.title)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(report.id, params.spreads)),
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
tagsNode,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`)
|
||||
),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null,
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.reportsStatus}: `),
|
||||
renderStateChip(normalizeStatus(report.status) === "RESOLVED" ? "mutuals" : "whole", "◆", String(report.status || "OPEN").toUpperCase()),
|
||||
renderReportStatusSetter(report)
|
||||
)
|
||||
: null,
|
||||
ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null,
|
||||
renderDocumentActions('reports', report.id, [
|
||||
a({ href: "/tasks?filter=create", class: "filter-btn" }, i18n.reportsCreateTaskButton)
|
||||
])
|
||||
);
|
||||
|
||||
const opinionsBar = renderOpinionsVoting('/reports/opinions', report.id, report.opinions, null, report.opinions_inhabitants);
|
||||
|
||||
const reportMain = div({ class: "tribe-main" },
|
||||
(details || report.image) ? div({ class: "job-section" },
|
||||
details || null,
|
||||
report.image ? renderMediaBlob(report.image, { class: "report-detail-image" }) : null
|
||||
) : null,
|
||||
details ? div({ class: "job-section" }, details) : null,
|
||||
renderPhotoGallery(report, 'report'),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(report.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(report.author)
|
||||
),
|
||||
opinionsBar,
|
||||
renderReportCommentsSection(report.id, comments)
|
||||
renderEngagement(report.id, opinionsBar, renderReportCommentsSection(report.id, comments))
|
||||
);
|
||||
|
||||
return template(
|
||||
report.title,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(i18n.reportsTitle), p(i18n.reportsDescription)),
|
||||
div(
|
||||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/reports" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, i18n.reportsFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, i18n.reportsFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, i18n.reportsFilterFeatures),
|
||||
button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, i18n.reportsFilterBugs),
|
||||
button({ type: "submit", name: "filter", value: "abuse", class: btnClass("abuse") }, i18n.reportsFilterAbuse),
|
||||
button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, i18n.reportsFilterContent),
|
||||
button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, i18n.reportsFilterConfirmed),
|
||||
button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, i18n.reportsFilterOpen),
|
||||
button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, i18n.reportsFilterUnderReview),
|
||||
button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, i18n.reportsFilterResolved),
|
||||
button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, i18n.reportsFilterInvalid),
|
||||
{ method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, String(i18n.reportsFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, String(i18n.reportsFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: btnClass("recent") }, String(i18n.reportsFilterRecent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "top", class: btnClass("top") }, String(i18n.reportsFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, String(i18n.reportsFilterFeatures).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, String(i18n.reportsFilterBugs).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, String(i18n.reportsFilterContent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, String(i18n.reportsFilterConfirmed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton)
|
||||
),
|
||||
form(
|
||||
{ method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" },
|
||||
button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, String(i18n.reportsFilterOpen).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, String(i18n.reportsFilterUnderReview).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, String(i18n.reportsFilterResolved).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, String(i18n.reportsFilterInvalid).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_low", class: btnClass("sev_low") }, String(i18n.reportsSeverityLow).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_medium", class: btnClass("sev_medium") }, String(i18n.reportsSeverityMedium).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_high", class: btnClass("sev_high") }, String(i18n.reportsSeverityHigh).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sev_critical", class: btnClass("sev_critical") }, String(i18n.reportsSeverityCritical).toUpperCase())
|
||||
)
|
||||
),
|
||||
div({ class: "tribe-details" }, reportSide, reportMain)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const moment = require("../server/node_modules/moment");
|
|||
const { renderTextWithStyles } = require('../backend/renderTextWithStyles');
|
||||
const { renderUrl } = require('../backend/renderUrl');
|
||||
const { sanitizeHtml } = require('../backend/sanitizeHtml');
|
||||
const { renderZoomableImage } = require('./gallery_view');
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const userId = config.keys.id;
|
||||
|
||||
|
|
@ -41,15 +42,14 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
const contentTypes = [
|
||||
"post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes",
|
||||
"report", "task", "event", "bookmark", "image", "audio", "video", "document", "torrent",
|
||||
"bankWallet", "bankClaim", "project", "job", "industry", "industryBlueprint", "forum", "vote", "contact", "pub", "map", "shop", "shopProduct", "chat", "pad", "all"
|
||||
"bankWallet", "bankClaim", "project", "job", "industry", "industryBlueprint", "forum", "vote", "contact", "map", "shop", "shopProduct", "chat", "pad", "all"
|
||||
];
|
||||
|
||||
const filterSelect = select(
|
||||
{
|
||||
id: "search-type",
|
||||
name: "type",
|
||||
class: "input-select",
|
||||
style: "position:relative; z-index:10;"
|
||||
class: "input-select search-select"
|
||||
},
|
||||
contentTypes.map(type =>
|
||||
option({
|
||||
|
|
@ -63,8 +63,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
{
|
||||
id: "results-per-page",
|
||||
name: "resultsPerPage",
|
||||
class: "input-select",
|
||||
style: "position:relative; z-index:10;margin-left:10px;"
|
||||
class: "input-select search-select"
|
||||
},
|
||||
option({ value: "100", selected: resultCount === "100" ? "selected" : undefined }, "100"),
|
||||
option({ value: "50", selected: resultCount === "50" ? "selected" : undefined }, "50"),
|
||||
|
|
@ -117,7 +116,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
const s = String(value).trim().replace(/&/g, '&');
|
||||
const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/);
|
||||
const src = m ? m[1] : s;
|
||||
return src.startsWith('&') ? img({ src: `/blob/${encodeURIComponent(src)}`, class: 'search-result-image' }) : null;
|
||||
return src.startsWith('&') ? renderZoomableImage(`/blob/${encodeURIComponent(src)}`, { imgClass: 'search-result-image' }) : null;
|
||||
};
|
||||
|
||||
const renderContentHtml = (content) => {
|
||||
|
|
@ -125,12 +124,12 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'post':
|
||||
return div({ class: 'search-post' },
|
||||
content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null,
|
||||
content.text ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: sanitizeHtml(content.text) })) : null
|
||||
content.text ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: sanitizeHtml(rewriteHashtagLinks(renderTextWithStyles(content.text))) })) : null
|
||||
);
|
||||
case 'about':
|
||||
return div({ class: 'search-about' },
|
||||
content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.name + ':'), span({ class: 'card-value' }, content.name)) : null,
|
||||
content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.description + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.image ? img({ src: `/image/64/${encodeURIComponent(content.image)}` }) : null
|
||||
);
|
||||
case 'feed': {
|
||||
|
|
@ -153,7 +152,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
const cleanEvDesc = rawEvDesc.replace(/!\[[^\]]*\]\(&[^)]+\.sha256\)/g, '').trim();
|
||||
return div({ class: 'search-event' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
cleanEvDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, cleanEvDesc)) : null,
|
||||
cleanEvDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, cleanEvDesc)) : null,
|
||||
blobImg(content.image || blobInEvDesc),
|
||||
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,
|
||||
|
|
@ -202,7 +201,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
return div({ class: 'search-tribe' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
(() => { const s = String(content.image || '').trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; return src.startsWith('&') ? img({ src: `/blob/${encodeURIComponent(src)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }); })(),
|
||||
content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, ...renderUrl(content.description))) : null,
|
||||
content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, ...renderUrl(content.description))) : null,
|
||||
content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLocationLabel + ':'), span({ class: 'card-value' }, ...renderUrl(content.location))) : null,
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel + ':'), span({ class: 'card-value' }, content.isAnonymous ? i18n.tribePrivate : i18n.tribePublic)),
|
||||
content.inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeModeLabel + ':'), span({ class: 'card-value' }, String(content.inviteMode).toUpperCase())) : null,
|
||||
|
|
@ -218,7 +217,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'audio':
|
||||
return content.url ? div({ class: 'search-audio' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
br(),
|
||||
audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' }),
|
||||
br(),
|
||||
|
|
@ -231,10 +230,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'image':
|
||||
return content.url ? div({ class: 'search-image' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
br(),
|
||||
img({ src: `/blob/${encodeURIComponent(content.url)}` }),
|
||||
renderZoomableImage(`/blob/${encodeURIComponent(content.url)}`, { imgClass: 'search-result-image' }),
|
||||
br(),
|
||||
content.tags && content.tags.length
|
||||
? div({ class: 'card-tags' }, content.tags.map(tag =>
|
||||
|
|
@ -245,7 +243,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'video':
|
||||
return content.url ? div({ class: 'search-video' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, 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(),
|
||||
|
|
@ -276,7 +274,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'torrent':
|
||||
return div({ class: 'search-torrent' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentTitleLabel || i18n.title) + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || i18n.searchDescription) + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.size ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentSizeLabel || 'Size') + ':'), span({ class: 'card-value' }, String(content.size))) : null,
|
||||
content.tags && content.tags.length
|
||||
? div({ class: 'card-tags' }, content.tags.map(tag =>
|
||||
|
|
@ -287,7 +285,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'market':
|
||||
return div({ class: 'search-market' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, 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,
|
||||
|
|
@ -297,7 +295,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
content.seller ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemSeller + ':'), span({ class: 'card-value' }, userLink(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.condition ? div({ class: 'card-field' }, 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' },
|
||||
|
|
@ -328,8 +326,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'bookmark':
|
||||
return div({ class: 'search-bookmark' },
|
||||
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.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkCategory + ':'), span({ class: 'card-value' }, content.category)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())) : null,
|
||||
content.tags && content.tags.length
|
||||
? div({ class: 'card-tags' }, content.tags.map(tag =>
|
||||
|
|
@ -340,7 +337,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'task':
|
||||
return div({ class: 'search-task' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field', style: 'display:flex;flex-direction:column;' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), p({ class: 'card-value', style: 'white-space:pre-wrap;margin-top:4px;' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field card-field-stacked' }, p({ class: 'card-value card-value-pre' }, 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,
|
||||
|
|
@ -360,7 +357,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
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,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
br(),
|
||||
blobImg(content.image),
|
||||
br(),
|
||||
|
|
@ -393,8 +390,8 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'curriculum':
|
||||
return div({ class: 'search-curriculum' },
|
||||
content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvNameLabel + ':'), span({ class: 'card-value' }, content.name)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.photo ? img({ src: `/blob/${encodeURIComponent(content.photo)}`, class: 'curriculum-photo' }) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.photo ? renderZoomableImage(`/blob/${encodeURIComponent(content.photo)}`, { imgClass: 'curriculum-photo' }) : null,
|
||||
content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null,
|
||||
content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null,
|
||||
content.preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvPreferencesLabel + ':'), span({ class: 'card-value' }, content.preferences)) : null,
|
||||
|
|
@ -474,7 +471,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
return div({ class: 'search-forum' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null,
|
||||
content.text ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.text)) : null
|
||||
content.text ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, content.text)) : null
|
||||
);
|
||||
case 'vote':
|
||||
return div({ class: 'search-vote-link' },
|
||||
|
|
@ -492,7 +489,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
return div({ class: 'search-shop' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
blobImg(content.image),
|
||||
content.shortDescription ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.shortDescription)) : null,
|
||||
content.shortDescription ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.shortDescription)) : null,
|
||||
content.visibility ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopStatus || 'STATUS') + ':'), span({ class: 'card-value' }, content.visibility)) : null,
|
||||
content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.searchLocationLabel || 'LOCATION') + ':'), span({ class: 'card-value' }, content.location)) : null,
|
||||
content.tags && content.tags.length
|
||||
|
|
@ -503,7 +500,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
return div({ class: 'search-shop-product' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
blobImg(content.image),
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.searchPriceLabel || 'PRICE') + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null,
|
||||
content.stock !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock)) : null
|
||||
);
|
||||
|
|
@ -515,7 +512,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
);
|
||||
return div({ class: 'search-chat' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatsTitle || 'Chat') + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, content.category)) : null,
|
||||
content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'STATUS') + ':'), span({ class: 'card-value' }, content.status)) : null
|
||||
);
|
||||
|
|
@ -546,7 +543,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'map':
|
||||
return div({ class: 'search-map' },
|
||||
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.mapDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null,
|
||||
content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null,
|
||||
content.lat && content.lng ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapLocationTitle + ':'), span({ class: 'card-value' }, `${content.lat}, ${content.lng}`)) : null,
|
||||
content.mapType ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapTypeLabel + ':'), span({ class: 'card-value' }, content.mapType)) : null,
|
||||
content.tags && content.tags.length
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span } = require("../server/node_modules/hyperaxe");
|
||||
const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span, img } = require("../server/node_modules/hyperaxe");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { template, selectedLanguage, i18n, setLanguage } = require('./main_views');
|
||||
const i18nBase = require("../client/assets/translations/i18n");
|
||||
const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model');
|
||||
|
||||
const snhUrl = "https://wiki.solarnethub.com/socialnet/overview";
|
||||
|
||||
|
|
@ -28,18 +29,37 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
const walletUrl = currentConfig.wallet.url;
|
||||
const walletUser = currentConfig.wallet.user;
|
||||
const walletFee = currentConfig.wallet.fee;
|
||||
const pubId = currentConfig.walletPub?.pubId || '';
|
||||
const currentWish = currentConfig.wish === 'mutuals' ? 'mutuals' : 'whole';
|
||||
const currentPmVisibility = currentConfig.pmVisibility === 'mutuals' ? 'mutuals' : 'whole';
|
||||
|
||||
const themeElements = [
|
||||
option({ value: "Dark-SNH", selected: theme === "Dark-SNH" ? true : undefined }, "Dark-SNH"),
|
||||
option({ value: "Clear-SNH", selected: theme === "Clear-SNH" ? true : undefined }, "Clear-SNH"),
|
||||
option({ value: "Purple-SNH", selected: theme === "Purple-SNH" ? true : undefined }, "Purple-SNH"),
|
||||
option({ value: "Matrix-SNH", selected: theme === "Matrix-SNH" ? true : undefined }, "Matrix-SNH"),
|
||||
option({ value: "OasisMobile", selected: theme === "OasisMobile" ? true : undefined }, "Oasis-Mobile")
|
||||
option({ value: "Dark-SNH", ...(theme === "Dark-SNH" ? true : undefined ? { selected: true } : {})}, "Dark-SNH"),
|
||||
option({ value: "Clear-SNH", ...(theme === "Clear-SNH" ? true : undefined ? { selected: true } : {})}, "Clear-SNH"),
|
||||
option({ value: "Purple-SNH", ...(theme === "Purple-SNH" ? true : undefined ? { selected: true } : {})}, "Purple-SNH"),
|
||||
option({ value: "Matrix-SNH", ...(theme === "Matrix-SNH" ? true : undefined ? { selected: true } : {})}, "Matrix-SNH"),
|
||||
option({ value: "OasisMobile", ...(theme === "OasisMobile" ? true : undefined ? { selected: true } : {})}, "Oasis-Mobile")
|
||||
];
|
||||
|
||||
const activeWorkflow = currentWorkflow(currentConfig) || '';
|
||||
const modOn = (name) => (currentConfig.modules || {})[`${name}Mod`] === 'on';
|
||||
|
||||
const workflowSection = section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.workflowsTitle),
|
||||
p(i18n.workflowsDescription),
|
||||
form(
|
||||
{ action: "/settings/workflow", method: "POST" },
|
||||
select({ name: "workflow" },
|
||||
...WORKFLOWS.map(w => activeWorkflow === w.key
|
||||
? option({ value: w.key, selected: true }, i18n[`workflow_${w.key}`])
|
||||
: option({ value: w.key }, i18n[`workflow_${w.key}`]))
|
||||
),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.workflowsSet)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const languageOption = (longName, shortName) => {
|
||||
return shortName === selectedLanguage
|
||||
? option({ value: shortName, selected: true }, longName)
|
||||
|
|
@ -69,19 +89,6 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
updateButton
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.theme),
|
||||
p(i18n.themeIntro),
|
||||
form(
|
||||
{ action: "/settings/theme", method: "post" },
|
||||
select({ name: "theme" }, ...themeElements),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.setTheme)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.language),
|
||||
|
|
@ -115,19 +122,37 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
{ action: "/settings/ux", method: "POST" },
|
||||
(() => {
|
||||
const aiNavEnabled = currentConfig.modules && currentConfig.modules.aiNavMod === 'on';
|
||||
const opts = [
|
||||
option({ value: "blocks", selected: (currentConfig.ux?.current !== "ainav") ? true : undefined }, i18n.uxModeMenus || "Blocks")
|
||||
];
|
||||
if (aiNavEnabled) {
|
||||
opts.push(option({ value: "ainav", selected: currentConfig.ux?.current === "ainav" ? true : undefined }, i18n.uxModeAINav || "AI"));
|
||||
}
|
||||
return select({ name: "ux" }, ...opts);
|
||||
const chatsEnabled = currentConfig.modules && currentConfig.modules.chatsMod === 'on';
|
||||
const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : "blocks";
|
||||
const uxCard = (value, title, image) => label({ class: "welcome-ux-option" },
|
||||
input({ type: "radio", name: "ux", value, ...(cur === value ? { checked: true } : {}) }),
|
||||
img({ src: image, class: "welcome-ux-shot", alt: title }),
|
||||
span({ class: "welcome-ux-label" }, title)
|
||||
);
|
||||
return div({ class: "welcome-ux-grid" },
|
||||
uxCard("blocks", i18n.uxModeMenus || "Blocks", "/assets/images/ux-blocks.png"),
|
||||
chatsEnabled ? uxCard("chats", i18n.chatsTitle || "Chats", "/assets/images/ux-chats.png") : null,
|
||||
aiNavEnabled ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null
|
||||
);
|
||||
})(),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
workflowSection,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.theme),
|
||||
p(i18n.themeIntro),
|
||||
form(
|
||||
{ action: "/settings/theme", method: "post" },
|
||||
select({ name: "theme" }, ...themeElements),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.setTheme)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.homePageTitle),
|
||||
|
|
@ -135,24 +160,58 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
form(
|
||||
{ action: "/settings/home-page", method: "POST" },
|
||||
select({ name: "homePage" },
|
||||
option({ value: "activity", selected: currentConfig.homePage === "activity" ? true : undefined }, i18n.activityTitle),
|
||||
option({ value: "ai", selected: currentConfig.homePage === "ai" ? true : undefined }, i18n.aiTitle),
|
||||
option({ value: "trending", selected: currentConfig.homePage === "trending" ? true : undefined }, i18n.trendingTitle),
|
||||
option({ value: "opinions", selected: currentConfig.homePage === "opinions" ? true : undefined }, i18n.opinionsTitle),
|
||||
option({ value: "forum", selected: currentConfig.homePage === "forum" ? true : undefined }, i18n.forumTitle),
|
||||
option({ value: "feed", selected: currentConfig.homePage === "feed" ? true : undefined }, i18n.feedTitle),
|
||||
option({ value: "mentions", selected: currentConfig.homePage === "mentions" ? true : undefined }, i18n.mentions),
|
||||
option({ value: "inbox", selected: currentConfig.homePage === "inbox" ? true : undefined }, i18n.inbox),
|
||||
option({ value: "agenda", selected: currentConfig.homePage === "agenda" ? true : undefined }, i18n.agendaTitle),
|
||||
option({ value: "favorites", selected: currentConfig.homePage === "favorites" ? true : undefined }, i18n.favoritesTitle),
|
||||
option({ value: "stats", selected: currentConfig.homePage === "stats" ? true : undefined }, i18n.statsTitle),
|
||||
option({ value: "blockexplorer", selected: currentConfig.homePage === "blockexplorer" ? true : undefined }, i18n.blockchain)
|
||||
...[
|
||||
{ value: "activity", label: i18n.activityTitle },
|
||||
{ value: "ai", label: i18n.aiTitle, mod: "ai" },
|
||||
{ value: "trending", label: i18n.trendingTitle, mod: "trending" },
|
||||
{ value: "forum", label: i18n.forumTitle, mod: "forum" },
|
||||
{ value: "feed", label: i18n.feedTitle, mod: "feed" },
|
||||
{ value: "chats", label: i18n.chatsTitle, mod: "chats" },
|
||||
{ value: "inbox", label: i18n.inbox },
|
||||
{ value: "mentions", label: i18n.mentions },
|
||||
{ value: "agenda", label: i18n.agendaTitle, mod: "agenda" },
|
||||
{ value: "market", label: i18n.marketTitle, mod: "market" },
|
||||
{ value: "favorites", label: i18n.favoritesTitle, mod: "favorites" }
|
||||
].filter(o => !o.mod || modOn(o.mod)).map(o => currentConfig.homePage === o.value
|
||||
? option({ value: o.value, selected: true }, o.label)
|
||||
: option({ value: o.value }, o.label))
|
||||
),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.saveHomePage)
|
||||
)
|
||||
)
|
||||
),
|
||||
modOn('ai') ? section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.aiTitle),
|
||||
p(i18n.aiSettingsDescription),
|
||||
form(
|
||||
{ action: "/settings/ai", method: "POST" },
|
||||
input({
|
||||
type: "text",
|
||||
id: "ai_prompt",
|
||||
name: "ai_prompt",
|
||||
placeholder: aiPrompt,
|
||||
value: aiPrompt,
|
||||
maxlength: "128",
|
||||
required: true
|
||||
}), br(),
|
||||
label({ for: "aiSuggestions", class: "lan-checkbox-label" },
|
||||
input({
|
||||
type: "checkbox",
|
||||
id: "aiSuggestions",
|
||||
name: "ai_suggestions",
|
||||
value: "on",
|
||||
class: "lan-checkbox-input",
|
||||
checked: currentConfig.ai?.suggestions !== false ? true : undefined
|
||||
}),
|
||||
span({ class: "lan-checkbox-text" }, i18n.aiSuggestionsEnable)
|
||||
),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
) : null,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.ssbLogStream),
|
||||
|
|
@ -221,9 +280,9 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
form(
|
||||
{ action: "/settings/wish", method: "POST" },
|
||||
select({ name: "wish" },
|
||||
option({ value: "whole", selected: currentWish === "whole" ? true : undefined }, i18n.settingsWishWhole),
|
||||
option({ value: "mutuals", selected: currentWish === "mutuals" ? true : undefined }, i18n.settingsWishMutuals),
|
||||
option({ value: "only-lan", selected: currentWish === "only-lan" ? true : undefined }, i18n.settingsWishOnlyLan || "Only LAN")
|
||||
option({ value: "whole", ...(currentWish === "whole" ? true : undefined ? { selected: true } : {})}, i18n.settingsWishWhole),
|
||||
option({ value: "mutuals", ...(currentWish === "mutuals" ? true : undefined ? { selected: true } : {})}, i18n.settingsWishMutuals),
|
||||
option({ value: "only-lan", ...(currentWish === "only-lan" ? true : undefined ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN")
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
|
|
@ -236,14 +295,14 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
form(
|
||||
{ action: "/settings/pm-visibility", method: "POST" },
|
||||
select({ name: "pmVisibility" },
|
||||
option({ value: "whole", selected: currentPmVisibility === "whole" ? true : undefined }, i18n.settingsPmVisibilityWhole),
|
||||
option({ value: "mutuals", selected: currentPmVisibility === "mutuals" ? true : undefined }, i18n.settingsPmVisibilityMutuals)
|
||||
option({ value: "whole", ...(currentPmVisibility === "whole" ? true : undefined ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
||||
option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? true : undefined ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
modOn('wallet') ? section(
|
||||
{ id: "wallet" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.wallet),
|
||||
|
|
@ -264,45 +323,9 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
button({ type: "submit" }, i18n.walletConfiguration)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.pubIdTitle || "PUB Wallet"),
|
||||
p(i18n.pubIdDescription || "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI)."),
|
||||
form(
|
||||
{ action: "/settings/pub-id", method: "POST" },
|
||||
input({
|
||||
type: "text",
|
||||
id: "pub_id",
|
||||
name: "pub_id",
|
||||
value: pubId,
|
||||
placeholder: i18n.pubIdPlaceholder || "@example.ed25519"
|
||||
}), br(),
|
||||
button({ type: "submit" }, i18n.pubIdSave || "Save configuration")
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.aiTitle),
|
||||
p(i18n.aiSettingsDescription),
|
||||
form(
|
||||
{ action: "/settings/ai", method: "POST" },
|
||||
input({
|
||||
type: "text",
|
||||
id: "ai_prompt",
|
||||
name: "ai_prompt",
|
||||
placeholder: aiPrompt,
|
||||
value: aiPrompt,
|
||||
maxlength: "128",
|
||||
required: true
|
||||
}), br(),
|
||||
button({ type: "submit" }, i18n.aiConfiguration)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
{ id: "fediverse" },
|
||||
) : null,
|
||||
modOn('fediverse') ? section(
|
||||
{ id: "multiverse" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.fediverseSettingsTitle),
|
||||
div({ class: "fediverse-network" },
|
||||
|
|
@ -333,7 +356,7 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) =
|
|||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
) : null,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.indexes),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, progress, video, table, tr, td } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderInviteQrCard } = require("./main_views")
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderEngagement, renderInviteQrCard , renderSpreadEditWarning, renderContentActions } = require("./main_views")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
|
|
@ -60,18 +61,14 @@ const renderModeButtons = (currentFilter) =>
|
|||
)
|
||||
)
|
||||
|
||||
const renderFavoriteToggle = (shop, returnTo) =>
|
||||
form(
|
||||
{ method: "POST", action: shop.isFavorite ? `/shops/favorites/remove/${encodeURIComponent(shop.key)}` : `/shops/favorites/add/${encodeURIComponent(shop.key)}` },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, shop.isFavorite ? i18n.shopRemoveFavorite : i18n.shopAddFavorite)
|
||||
)
|
||||
|
||||
const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => {
|
||||
const returnTo = buildReturnTo(filter, params)
|
||||
const isAuthor = String(shop.author) === String(userId)
|
||||
|
||||
return div({ class: "tribe-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreadMap && params.spreadMap.get(shop.key) || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title })
|
||||
),
|
||||
div({ class: "tribe-card-image-wrapper" },
|
||||
a({ href: `/shops/${encodeURIComponent(shop.key)}` },
|
||||
renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' })
|
||||
|
|
@ -89,12 +86,6 @@ const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => {
|
|||
),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(shop.key, params.spreadMap && params.spreadMap.get(shop.key))),
|
||||
div({ class: "card-visit-btn-centered" },
|
||||
form({ method: 'GET', action: `/shops/${encodeURIComponent(shop.key)}` },
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.viewShop || i18n.shopVisitShop || 'View Shop')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -126,9 +117,6 @@ const renderProductCard = (product, shopId, returnTo) => {
|
|||
if (!isAuthor && stock > 0) {
|
||||
actions.push(a({ href: productUrl, class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy));
|
||||
}
|
||||
actions.push(form({ method: "POST", action: product.isFavorite ? `/shops/favorites/remove/${encodeURIComponent(product.key)}` : `/shops/favorites/add/${encodeURIComponent(product.key)}` },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, product.isFavorite ? i18n.shopRemoveFavorite : i18n.shopAddFavorite)));
|
||||
return actions.length ? div({ class: "shop-product-actions" }, ...actions) : null;
|
||||
})()
|
||||
)
|
||||
|
|
@ -136,50 +124,23 @@ const renderProductCard = (product, shopId, returnTo) => {
|
|||
}
|
||||
|
||||
const renderCommentsSection = (parentId, returnTo, comments = []) => {
|
||||
const count = Array.isArray(comments) ? comments.length : 0
|
||||
return div({ class: "vote-comments-section market-comments" },
|
||||
div({ class: "comments-count" }, span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), span({ class: "card-value" }, String(count))),
|
||||
div({ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form({ method: "POST", action: `/shops/${encodeURIComponent(parentId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
textarea({ name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
count
|
||||
? div({ class: "comments-list" },
|
||||
comments.map(c => {
|
||||
const author = c.value?.author || ""
|
||||
const ts = c.value?.timestamp || c.timestamp
|
||||
const text = c.value?.content?.text || ""
|
||||
const rootId = c.value?.content?.fork || c.value?.content?.root || null
|
||||
return div({ class: "votations-comment-card" },
|
||||
span({ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
ts ? span(" | ", span({ class: "votations-comment-date" }, moment(ts).format("YYYY/MM/DD HH:mm:ss"))) : "",
|
||||
ts && rootId ? span(" | ", a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, moment(ts).fromNow())) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
)
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
)
|
||||
}
|
||||
return renderSharedCommentsSection({
|
||||
action: `/shops/${encodeURIComponent(parentId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
const renderShopForm = (filter, shop = {}, params = {}) => {
|
||||
const isEdit = filter === "edit"
|
||||
const returnTo = safeText(params.returnTo) || buildReturnTo("all")
|
||||
return div({ class: "create-tribe-form" },
|
||||
h2(isEdit ? i18n.shopUpdateSectionTitle : i18n.shopCreateSectionTitle),
|
||||
isEdit ? (params.spreadWarning || null) : null,
|
||||
form({ action: isEdit ? `/shops/update/${encodeURIComponent(shop.key || "")}` : "/shops/create", method: "POST", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
label(i18n.title || "Title"), br,
|
||||
input({ type: "text", name: "title", required: true, placeholder: i18n.shopTitlePlaceholder || "Name of your shop", value: shop.title || "" }), br(),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.shopTitlePlaceholder || "Name of your shop", value: shop.title || "" }), br(),
|
||||
label(i18n.shopShortDescription), br,
|
||||
input({ type: "text", name: "shortDescription", required: true, maxlength: 160, placeholder: i18n.shopShortDescriptionPlaceholder || "Brief description of your shop", value: shop.shortDescription || "" }), br(),
|
||||
label(i18n.description || "Description"), br,
|
||||
|
|
@ -197,8 +158,8 @@ const renderShopForm = (filter, shop = {}, params = {}) => {
|
|||
isEdit ? null : label(i18n.shopVisibility),
|
||||
isEdit ? null : br,
|
||||
isEdit ? null : select({ name: "visibility" },
|
||||
option({ value: "OPEN", selected: (shop.visibility || "OPEN") === "OPEN" }, i18n.shopOpen),
|
||||
option({ value: "CLOSED", selected: shop.visibility === "CLOSED" }, i18n.shopClosed)
|
||||
option({ value: "OPEN", ...((shop.visibility || "OPEN") === "OPEN" ? { selected: true } : {})}, i18n.shopOpen),
|
||||
option({ value: "CLOSED", ...(shop.visibility === "CLOSED" ? { selected: true } : {})}, i18n.shopClosed)
|
||||
),
|
||||
isEdit ? null : br(),
|
||||
br(),
|
||||
|
|
@ -207,14 +168,15 @@ const renderShopForm = (filter, shop = {}, params = {}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "") => {
|
||||
const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "", spreadWarning = null) => {
|
||||
return div({ class: "create-tribe-form" },
|
||||
h2(isEdit ? i18n.shopProductUpdate : i18n.shopProductAdd),
|
||||
isEdit ? spreadWarning : null,
|
||||
form({ action: isEdit ? `/shops/product/update/${encodeURIComponent(product.key || "")}` : "/shops/product/create", method: "POST", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "shopId", value: shopId }),
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
label(i18n.title || "Title"), br,
|
||||
input({ type: "text", name: "title", required: true, value: product.title || "" }), br(),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, value: product.title || "" }), br(),
|
||||
label(i18n.description || "Description"), br,
|
||||
textarea({ name: "description", rows: 4 }, product.description || ""), br,
|
||||
label(i18n.shopProductPrice), br,
|
||||
|
|
@ -241,66 +203,47 @@ const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "")
|
|||
}
|
||||
|
||||
exports.shopsView = async (shops, filter, shopToEdit = null, params = {}) => {
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(shopToEdit && (shopToEdit.key || shopToEdit.id)) };
|
||||
const q = safeText(params.q || "")
|
||||
const sort = safeText(params.sort || "recent")
|
||||
const list = safeArr(shops)
|
||||
|
||||
const title =
|
||||
filter === "mine" ? i18n.shopMineSectionTitle :
|
||||
filter === "recent" ? i18n.shopRecentSectionTitle :
|
||||
filter === "top" ? i18n.shopTopSectionTitle :
|
||||
filter === "products" ? i18n.shopProductsSectionTitle :
|
||||
filter === "prices" ? (i18n.shopPricesSectionTitle || "Products by Price") :
|
||||
filter === "favorites" ? i18n.shopFavoritesSectionTitle :
|
||||
filter === "create" ? i18n.shopCreateSectionTitle :
|
||||
filter === "edit" ? i18n.shopUpdateSectionTitle :
|
||||
i18n.shopAllSectionTitle
|
||||
|
||||
const title = i18n.shopsTitle
|
||||
const isForm = filter === "create" || filter === "edit"
|
||||
|
||||
const viewerClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetShops)
|
||||
const header = [
|
||||
div({ class: "tags-header" },
|
||||
h2(title),
|
||||
p(i18n.shopDescription)
|
||||
),
|
||||
div({ class: "shop-title-row" }, renderReachChip(viewerClearnet, i18n)),
|
||||
br()
|
||||
]
|
||||
|
||||
const searchBar = div({ class: "filters" },
|
||||
form({ method: "GET", action: "/shops" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", placeholder: i18n.shopSearchPlaceholder, value: q }),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.search),
|
||||
br()
|
||||
)
|
||||
)
|
||||
|
||||
const sortedProducts = filter === "products"
|
||||
? [...list].sort((a, b) => safeArr(b.opinions_inhabitants).length - safeArr(a.opinions_inhabitants).length)
|
||||
: filter === "prices"
|
||||
? [...list].sort((a, b) => Number(b.price || 0) - Number(a.price || 0))
|
||||
: list
|
||||
const isProducts = filter === "products" || filter === "prices"
|
||||
|
||||
return template(
|
||||
title,
|
||||
section(...header),
|
||||
section(div({ class: "tags-header" }, h2(title), p(i18n.shopDescription))),
|
||||
section(renderModeButtons(filter)),
|
||||
!isForm ? section(searchBar) : null,
|
||||
!isForm
|
||||
? section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/shops", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.shopSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
select({ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {}) }, i18n.documentSortRecent),
|
||||
option({ value: "top", ...(sort === "top" ? { selected: true } : {}) }, i18n.documentSortTop)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
section(
|
||||
isForm
|
||||
? renderShopForm(filter, filter === "edit" ? (shopToEdit || {}) : {}, params)
|
||||
: filter === "products" || filter === "prices"
|
||||
: isProducts
|
||||
? div({ class: "shop-products-grid" },
|
||||
sortedProducts.length
|
||||
? sortedProducts.map(prod => renderProductCard(prod, prod.shopId, buildReturnTo(filter, params)))
|
||||
: p(i18n.shopNoItems)
|
||||
list.length
|
||||
? list.map(prod => renderProductCard(prod, prod.shopId, buildReturnTo(filter, { q })))
|
||||
: p(i18n.shopNoProducts)
|
||||
)
|
||||
: div({ class: "tribe-grid" },
|
||||
list.length
|
||||
? list.map(shop => renderShopCard(shop, filter, { q, sort, spreadMap: params.spreadMap }))
|
||||
? list.map(shopItem => renderShopCard(shopItem, filter, params))
|
||||
: p(i18n.shopNoItems)
|
||||
)
|
||||
)
|
||||
|
|
@ -309,13 +252,15 @@ exports.shopsView = async (shops, filter, shopToEdit = null, params = {}) => {
|
|||
|
||||
exports.singleShopView = async (shop, filter, products = [], comments = [], params = {}) => {
|
||||
const q = safeText(params.q || "")
|
||||
const sort = safeText(params.sort || "recent")
|
||||
const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort })
|
||||
const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q })
|
||||
const isAuthor = String(shop.author) === String(userId)
|
||||
const fullShareUrl = `/shops/${encodeURIComponent(shop.key)}`
|
||||
const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetShops === true)
|
||||
products = safeArr(products)
|
||||
|
||||
const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetShops && shop.visibility !== 'CLOSED');
|
||||
const shopSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(shop.key, null, { spread: params.spreads || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, shop.title || i18n.shopUntitled)
|
||||
),
|
||||
|
|
@ -327,21 +272,13 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para
|
|||
renderLifespanChip(shop.lifetime, i18n),
|
||||
renderReachChip(isClearnet, i18n)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(shop.key, params.spreads)),
|
||||
renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }),
|
||||
shop.description ? p({ class: "tribe-side-description" }, ...renderUrl(shop.description)) : null,
|
||||
div({ class: "shop-share" },
|
||||
span({ class: "tribe-info-label" }, `${i18n.shopShareUrl}: `),
|
||||
input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" })
|
||||
),
|
||||
table({ class: "tribe-info-table" },
|
||||
tr(
|
||||
td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, new Date(shop.createdAt).toLocaleString())
|
||||
),
|
||||
tr(
|
||||
td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author))
|
||||
),
|
||||
shop.location ? tr(
|
||||
td({ class: "tribe-info-label" }, i18n.shopLocation),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, ...renderUrl(shop.location))
|
||||
|
|
@ -353,36 +290,15 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para
|
|||
shop.url ? tr(
|
||||
td({ class: "tribe-info-label" }, i18n.shopUrl),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, ...renderUrl(shop.url))
|
||||
) : null
|
||||
) : null,
|
||||
tr(
|
||||
td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author))
|
||||
)
|
||||
),
|
||||
renderMapEmbed(params.mapData, shop.mapUrl),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`)
|
||||
),
|
||||
div({ class: "tribe-side-actions" },
|
||||
renderFavoriteToggle(shop, returnTo),
|
||||
shop.author && String(shop.author) !== String(userId)
|
||||
? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: shop.author }), button({ type: "submit", class: "tribe-action-btn" }, i18n.privateMessage))
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "GET", action: `/shops/edit/${encodeURIComponent(shop.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.shopUpdate)
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "GET", action: `/shops/${encodeURIComponent(shop.key)}/orders` },
|
||||
button({ type: "submit", class: "tribe-action-btn" },
|
||||
i18n.shopOrdersTitle || "Orders",
|
||||
Number(shop.pendingOrders || 0) > 0 ? span({ class: "shop-orders-badge", title: i18n.shopOrdersPending || "Pending orders" }, ` ● ${shop.pendingOrders}`) : null
|
||||
)
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "POST", action: `/shops/delete/${encodeURIComponent(shop.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.shopDelete)
|
||||
)
|
||||
: null
|
||||
),
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions shop-visibility-row" },
|
||||
span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `),
|
||||
|
|
@ -416,6 +332,26 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para
|
|||
shop.encrypted ? i18n.shopMakePublic : i18n.shopMakePrivate))
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions" },
|
||||
form({ method: "GET", action: `/shops/${encodeURIComponent(shop.key)}/orders` },
|
||||
button({ type: "submit", class: "tribe-action-btn" },
|
||||
i18n.shopOrdersTitle || "Orders",
|
||||
Number(shop.pendingOrders || 0) > 0 ? span({ class: "shop-orders-badge", title: i18n.shopOrdersPending || "Pending orders" }, ` ● ${shop.pendingOrders}`) : null
|
||||
)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions owner-actions" },
|
||||
form({ method: "GET", action: `/shops/edit/${encodeURIComponent(shop.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.shopUpdate)
|
||||
),
|
||||
form({ method: "POST", action: `/shops/delete/${encodeURIComponent(shop.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.shopDelete)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
safeArr(shop.tags).length
|
||||
? div({ class: "tribe-side-tags" }, safeArr(shop.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||
: null
|
||||
|
|
@ -436,6 +372,7 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para
|
|||
|
||||
return template(
|
||||
shop.title || i18n.shopTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.shopsTitle), p(i18n.shopDescription))),
|
||||
section(renderModeButtons(filter)),
|
||||
section(
|
||||
div({ class: "tribe-details" },
|
||||
|
|
@ -451,89 +388,107 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) =>
|
|||
const isAuthor = String(product.author) === String(userId)
|
||||
const stock = Number(product.stock) || 0
|
||||
|
||||
return template(
|
||||
product.title || i18n.shopProductTitle,
|
||||
section(renderModeButtons("products")),
|
||||
section(
|
||||
div({ class: "shop-detail" },
|
||||
div({ class: "bookmark-topbar transfer-topbar-single" },
|
||||
div({ class: "bookmark-topbar-left" },
|
||||
product.author && String(product.author) !== String(userId)
|
||||
? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: product.author }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage))
|
||||
: null,
|
||||
form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, `← ${i18n.shopBackToShop}`))
|
||||
),
|
||||
isAuthor
|
||||
? div({ class: "bookmark-actions transfer-actions" },
|
||||
form({ method: "GET", action: `/shops/product/edit/${encodeURIComponent(product.key)}` },
|
||||
input({ type: "hidden", name: "shopId", value: product.shopId }),
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "update-btn", type: "submit" }, i18n.shopUpdate)
|
||||
),
|
||||
form({ method: "POST", action: `/shops/product/delete/${encodeURIComponent(product.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "delete-btn", type: "submit" }, i18n.shopDelete)
|
||||
)
|
||||
)
|
||||
: null
|
||||
),
|
||||
product.image ? div({ class: "shop-detail-media" }, renderMediaBlob(product.image)) : null,
|
||||
h2(product.title),
|
||||
renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length),
|
||||
product.description ? div({ class: "shop-detail-desc" }, ...renderUrl(product.description)) : null,
|
||||
div({ class: "shop-product-price" }, `${Number(product.price || 0).toFixed(6)} ECO`),
|
||||
div({ class: "confirmations-block stock-block" },
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.shopProductStock}: `),
|
||||
span({ class: "card-value" }, stock > 0 ? String(stock) : i18n.shopOutOfStock)
|
||||
),
|
||||
progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 })
|
||||
),
|
||||
!isAuthor && stock > 0
|
||||
? form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."),
|
||||
label(i18n.shopBuyDeliveryAddress || "Delivery address"),
|
||||
br(),
|
||||
textarea({ name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.shopBuyContact || "Contact"),
|
||||
br(),
|
||||
input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder || "email, phone, etc." }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.shopBuyNotes || "Notes"),
|
||||
br(),
|
||||
textarea({ name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy)
|
||||
)
|
||||
: null,
|
||||
br(),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, moment(product.createdAt).format("YYYY-MM-DD HH:mm")),
|
||||
" ",
|
||||
userLink(product.author)
|
||||
),
|
||||
!isAuthor && params.canRate
|
||||
? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants)
|
||||
: null
|
||||
const productSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(product.key, null, { spread: params.spreads || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, returnTo, reportTitle: product.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, product.title || i18n.shopProductUntitled)
|
||||
),
|
||||
renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length),
|
||||
product.image ? renderMediaBlob(product.image, null, { class: "tribe-detail-image" }) : null,
|
||||
div({ class: "card-date-highlight" }, `${Number(product.price || 0).toFixed(6)} ECO`),
|
||||
div({ class: "confirmations-block stock-block" },
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.shopProductStock}: `),
|
||||
span({ class: "card-value" }, stock > 0 ? String(stock) : i18n.shopOutOfStock)
|
||||
),
|
||||
progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 })
|
||||
),
|
||||
table({ class: "tribe-info-table jobs-info-table" },
|
||||
tr(
|
||||
td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, moment(product.createdAt).format("YYYY/MM/DD HH:mm"))
|
||||
),
|
||||
tr(
|
||||
td({ class: "tribe-info-value", colspan: "4" }, userLink(product.author))
|
||||
)
|
||||
),
|
||||
div({ class: "tribe-side-actions" },
|
||||
form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, `← ${i18n.shopBackToShop}`)
|
||||
)
|
||||
),
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions owner-actions" },
|
||||
form({ method: "GET", action: `/shops/product/edit/${encodeURIComponent(product.key)}` },
|
||||
input({ type: "hidden", name: "shopId", value: product.shopId }),
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "tribe-action-btn", type: "submit" }, i18n.shopUpdate)
|
||||
),
|
||||
form({ method: "POST", action: `/shops/product/delete/${encodeURIComponent(product.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "tribe-action-btn danger-btn", type: "submit" }, i18n.shopDelete)
|
||||
)
|
||||
)
|
||||
: null
|
||||
)
|
||||
|
||||
const productMain = div({ class: "tribe-main" },
|
||||
product.description
|
||||
? div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, i18n.marketItemDescription || i18n.shopDescription),
|
||||
p({ class: "tribe-side-description" }, ...renderUrl(product.description))
|
||||
)
|
||||
: null,
|
||||
!isAuthor && stock > 0
|
||||
? div({ class: "job-section" },
|
||||
form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."),
|
||||
label(i18n.shopBuyDeliveryAddress || "Delivery address"),
|
||||
br(),
|
||||
textarea({ name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.shopBuyContact || "Contact"),
|
||||
br(),
|
||||
input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder || "email, phone, etc." }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.shopBuyNotes || "Notes"),
|
||||
br(),
|
||||
textarea({ name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
renderEngagement(product.key,
|
||||
!isAuthor && params.canRate
|
||||
? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants)
|
||||
: null,
|
||||
renderCommentsSection(product.key, returnTo, comments)
|
||||
)
|
||||
)
|
||||
|
||||
return template(
|
||||
product.title || i18n.shopProductTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.shopsTitle), p(i18n.shopDescription))),
|
||||
section(renderModeButtons("products")),
|
||||
section(div({ class: "tribe-details" }, productSide, productMain))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
exports.editProductView = async (product, shopId, params = {}) => {
|
||||
const returnTo = safeText(params.returnTo) || `/shops/${encodeURIComponent(shopId)}`
|
||||
return template(
|
||||
i18n.shopProductUpdate,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(i18n.shopProductUpdate)),
|
||||
renderProductForm(shopId, product, true, returnTo)
|
||||
renderProductForm(shopId, product, true, returnTo, await renderSpreadEditWarning(product && (product.key || product.id)))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -586,7 +541,7 @@ exports.shopOrdersView = async (shop, orders) => {
|
|||
div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress || "Delivery address"}:`), span({ class: "card-value" }, String(o.deliveryAddress || ""))),
|
||||
o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact || "Contact"}:`), span({ class: "card-value" }, String(o.contact))) : null,
|
||||
o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes || "Notes"}:`), span({ class: "card-value" }, String(o.notes))) : null,
|
||||
p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY-MM-DD HH:mm"))),
|
||||
p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY/MM/DD HH:mm"))),
|
||||
orderActions(o)
|
||||
))
|
||||
return template(
|
||||
|
|
@ -623,7 +578,7 @@ exports.myPurchasesView = async (purchases) => {
|
|||
div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress || "Delivery address"}:`), span({ class: "card-value" }, String(o.deliveryAddress || ""))),
|
||||
o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact || "Contact"}:`), span({ class: "card-value" }, String(o.contact))) : null,
|
||||
o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes || "Notes"}:`), span({ class: "card-value" }, String(o.notes))) : null,
|
||||
p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY-MM-DD HH:mm"))),
|
||||
p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY/MM/DD HH:mm"))),
|
||||
buyerActions(o)
|
||||
))
|
||||
return template(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label, span, table, tr, td, img, video } = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions } = require("./main_views");
|
||||
const { renderPhotoGallery, renderGalleryFields, imagesOf } = require("./gallery_view");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ const renderTaskMediaBlob = (value, attrs = {}) => {
|
|||
const userId = config.keys.id;
|
||||
|
||||
const opt = (value, isSelected, text) =>
|
||||
option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text);
|
||||
option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text);
|
||||
|
||||
const safeArray = (v) => Array.isArray(v) ? v : [];
|
||||
|
||||
|
|
@ -57,95 +59,50 @@ const renderTaskOwnerActions = (task, returnTo) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "delete-btn" }, i18n.taskDeleteButton)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const renderTaskStatusRow = (task, returnTo) => {
|
||||
const st = normalizeStatus(task.status || "OPEN");
|
||||
return div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.taskStatus}: `),
|
||||
renderStateChip(st === "CLOSED" ? "hidden" : "mutuals", st === "CLOSED" ? "🔒" : "👁", statusLabel(task.status)),
|
||||
form(
|
||||
{ method: "POST", action: `/tasks/status/${encodeURIComponent(task.id)}`, class: "project-control-form project-control-form--status" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
select(
|
||||
{ name: "status", class: "project-control-select" },
|
||||
option({ value: "OPEN", selected: st === "OPEN" }, i18n.taskStatusOpen),
|
||||
option({ value: "IN-PROGRESS", selected: st === "IN-PROGRESS" }, i18n.taskStatusInProgress),
|
||||
option({ value: "CLOSED", selected: st === "CLOSED" }, i18n.taskStatusClosed)
|
||||
option({ value: "OPEN", ...(st === "OPEN" ? { selected: true } : {})}, i18n.taskStatusOpen),
|
||||
option({ value: "IN-PROGRESS", ...(st === "IN-PROGRESS" ? { selected: true } : {})}, i18n.taskStatusInProgress),
|
||||
option({ value: "CLOSED", ...(st === "CLOSED" ? { selected: true } : {})}, i18n.taskStatusClosed)
|
||||
),
|
||||
button({ class: "status-btn project-control-btn", type: "submit" }, setStatusLabel)
|
||||
button({ class: "status-btn project-control-btn", type: "submit" }, i18n.taskSetStatus)
|
||||
)
|
||||
];
|
||||
);
|
||||
};
|
||||
|
||||
const renderTaskAssignAction = (task, isAssignedToMe, returnTo) => {
|
||||
const st = normalizeStatus(task.status || "OPEN");
|
||||
if (st === "CLOSED") return null;
|
||||
return form(
|
||||
{ method: "POST", action: `/tasks/assign/${encodeURIComponent(task.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "filter-btn" }, isAssignedToMe ? i18n.taskUnassignButton : i18n.taskAssignButton)
|
||||
);
|
||||
return [
|
||||
span({ class: "card-label" }, `${i18n.taskAssignedTo}: `),
|
||||
renderStateChip(isAssignedToMe ? "mutuals" : "whole", isAssignedToMe ? "👤" : "○",
|
||||
String(isAssignedToMe ? i18n.taskAssignedChip : i18n.taskUnassignedChip).toUpperCase()),
|
||||
form(
|
||||
{ method: "POST", action: `/tasks/assign/${encodeURIComponent(task.id)}`, class: "project-control-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "status-btn project-control-btn" }, isAssignedToMe ? i18n.taskUnassignButton : i18n.taskAssignButton)
|
||||
)
|
||||
];
|
||||
};
|
||||
|
||||
const renderTaskCommentsSection = (taskId, comments = [], currentFilter = "all") => {
|
||||
const commentsCount = Array.isArray(comments) ? comments.length : 0;
|
||||
const returnTo = `/tasks/${encodeURIComponent(taskId)}?filter=${encodeURIComponent(currentFilter || "all")}`;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/tasks/${encodeURIComponent(taskId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
(() => {
|
||||
const visibleComments = (comments || []).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
});
|
||||
return visibleComments.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
visibleComments.map((c) => {
|
||||
const author = c.value && c.value.author ? c.value.author : "";
|
||||
const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
|
||||
const content = c.value && c.value.content ? c.value.content : {};
|
||||
const root = content.fork || content.root || "";
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(String(text)))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet);
|
||||
})()
|
||||
);
|
||||
return renderSharedCommentsSection({
|
||||
action: `/tasks/${encodeURIComponent(taskId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
const renderTaskStatusChip = (status) => {
|
||||
|
|
@ -176,10 +133,11 @@ const renderTaskItem = (task, filter, spreadInfo) => {
|
|||
const assignees = safeArray(task.assignees);
|
||||
const isPrivate = String(task.isPublic || "").toUpperCase() === "PRIVATE";
|
||||
|
||||
const heroNode = task.image
|
||||
const cover = imagesOf(task)[0]
|
||||
const heroNode = cover
|
||||
? div({ class: "tribe-card-image-wrapper" },
|
||||
a({ href: `/tasks/${encodeURIComponent(task.id)}` },
|
||||
renderTaskMediaBlob(task.image, { class: "tribe-card-hero-image" })
|
||||
renderTaskMediaBlob(cover, { class: "tribe-card-hero-image" })
|
||||
),
|
||||
div({ class: "tribe-visit-btn-wrapper" },
|
||||
form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` },
|
||||
|
|
@ -201,6 +159,10 @@ const renderTaskItem = (task, filter, spreadInfo) => {
|
|||
const end = task.endTime ? moment(task.endTime).format("YYYY/MM/DD HH:mm") : "";
|
||||
|
||||
return div({ class: "tribe-card task-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: spreadInfo || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title })
|
||||
),
|
||||
heroNode,
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
|
|
@ -212,16 +174,7 @@ const renderTaskItem = (task, filter, spreadInfo) => {
|
|||
(start || end) ? p({ class: "card-date-highlight" }, start && end ? `${start} → ${end}` : (start || end)) : null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(task.id, spreadInfo)),
|
||||
!heroNode
|
||||
? div({ class: "card-visit-btn-centered" },
|
||||
form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` },
|
||||
input({ type: "hidden", name: "filter", value: currentFilter }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.viewTask || "View Task")
|
||||
)
|
||||
)
|
||||
: null
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -230,19 +183,7 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => {
|
|||
const list = Array.isArray(tasks) ? tasks : [tasks];
|
||||
const currentFilter = filter || "all";
|
||||
|
||||
const title =
|
||||
currentFilter === "mine" ? i18n.taskMineSectionTitle :
|
||||
currentFilter === "create" ? i18n.taskCreateSectionTitle :
|
||||
currentFilter === "edit" ? i18n.taskUpdateSectionTitle :
|
||||
currentFilter === "open" ? i18n.taskOpenTitle :
|
||||
currentFilter === "in-progress" ? i18n.taskInProgressTitle :
|
||||
currentFilter === "closed" ? i18n.taskClosedTitle :
|
||||
currentFilter === "assigned" ? i18n.taskAssignedTitle :
|
||||
currentFilter === "priority-urgent" ? i18n.taskFilterUrgent :
|
||||
currentFilter === "priority-high" ? i18n.taskFilterHigh :
|
||||
currentFilter === "priority-medium" ? i18n.taskFilterMedium :
|
||||
currentFilter === "priority-low" ? i18n.taskFilterLow :
|
||||
i18n.taskAllSectionTitle;
|
||||
const title = i18n.tasksTitle;
|
||||
|
||||
const canSee = (t) => {
|
||||
const vis = String(t.isPublic || "").toUpperCase();
|
||||
|
|
@ -268,7 +209,8 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => {
|
|||
filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
|
||||
const editTask = list.find((t) => t.id === taskId) || {};
|
||||
const editTags = Array.isArray(editTask.tags) ? editTask.tags : [];
|
||||
const formData = currentFilter === "edit" ? editTask : (params.draft || {});
|
||||
const editTags = Array.isArray(formData.tags) ? formData.tags : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []);
|
||||
const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm");
|
||||
|
||||
const ret = typeof returnTo === "string" && returnTo.startsWith("/tasks")
|
||||
|
|
@ -287,40 +229,52 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/tasks" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAssigned),
|
||||
button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterOpen),
|
||||
button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterInProgress),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterClosed),
|
||||
button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterLow),
|
||||
button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMedium),
|
||||
button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterHigh),
|
||||
button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterUrgent),
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAssigned).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterOpen).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterInProgress).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterClosed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterLow).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMedium).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterHigh).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterUrgent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.taskCreateButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
currentFilter === "edit" || currentFilter === "create"
|
||||
? null
|
||||
: div({ class: "filters" },
|
||||
form({ method: "GET", action: "/tasks", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: currentFilter }),
|
||||
input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.taskSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
currentFilter === "edit" || currentFilter === "create"
|
||||
? div(
|
||||
{ class: "task-form" },
|
||||
currentFilter === "edit" ? await renderSpreadEditWarning(taskId) : null,
|
||||
form(
|
||||
{ action: currentFilter === "edit" ? `/tasks/update/${encodeURIComponent(taskId)}` : "/tasks/create", method: "POST", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: ret }),
|
||||
label(i18n.taskTitleLabel), br(),
|
||||
input({ type: "text", name: "title", required: true, value: currentFilter === "edit" ? (editTask.title || "") : (params.prefillTitle || "") }), br(),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, value: formData.title || params.prefillTitle || "" }), br(),
|
||||
label(i18n.taskDescriptionLabel), br(),
|
||||
textarea({ name: "description", required: true, placeholder: i18n.taskDescriptionPlaceholder, rows: "4" }, currentFilter === "edit" ? (editTask.description || "") : (params.prefillDescription || "")), br(),
|
||||
label(i18n.uploadMedia), br(),
|
||||
input({ type: "file", name: "image", accept: "image/*" }), br(),br(),
|
||||
textarea({ name: "description", required: true, placeholder: i18n.taskDescriptionPlaceholder, rows: "4" }, formData.description || params.prefillDescription || ""), br(),
|
||||
...renderGalleryFields(formData, currentFilter === "edit"),
|
||||
br(),
|
||||
label(i18n.taskStartTimeLabel), br(),
|
||||
input({
|
||||
type: "datetime-local",
|
||||
name: "startTime",
|
||||
required: true,
|
||||
min: currentFilter === "create" ? minCreate : undefined,
|
||||
value: currentFilter === "edit" && editTask.startTime ? moment(editTask.startTime).format("YYYY-MM-DDTHH:mm") : ""
|
||||
value: formData.startTime ? moment(formData.startTime).format("YYYY-MM-DDTHH:mm") : ""
|
||||
}), br(), br(),
|
||||
label(i18n.taskEndTimeLabel), br(),
|
||||
input({
|
||||
|
|
@ -328,25 +282,25 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => {
|
|||
name: "endTime",
|
||||
required: true,
|
||||
min: currentFilter === "create" ? minCreate : undefined,
|
||||
value: currentFilter === "edit" && editTask.endTime ? moment(editTask.endTime).format("YYYY-MM-DDTHH:mm") : ""
|
||||
value: formData.endTime ? moment(formData.endTime).format("YYYY-MM-DDTHH:mm") : ""
|
||||
}), br(), br(),
|
||||
label(i18n.taskPriorityLabel), br(),
|
||||
select(
|
||||
{ name: "priority", required: true },
|
||||
opt("URGENT", String(editTask.priority || "").toUpperCase() === "URGENT", i18n.taskPriorityUrgent),
|
||||
opt("HIGH", String(editTask.priority || "").toUpperCase() === "HIGH", i18n.taskPriorityHigh),
|
||||
opt("MEDIUM", String(editTask.priority || "").toUpperCase() === "MEDIUM", i18n.taskPriorityMedium),
|
||||
opt("LOW", !editTask.priority || String(editTask.priority || "").toUpperCase() === "LOW", i18n.taskPriorityLow)
|
||||
opt("URGENT", String(formData.priority || "").toUpperCase() === "URGENT", i18n.taskPriorityUrgent),
|
||||
opt("HIGH", String(formData.priority || "").toUpperCase() === "HIGH", i18n.taskPriorityHigh),
|
||||
opt("MEDIUM", String(formData.priority || "").toUpperCase() === "MEDIUM", i18n.taskPriorityMedium),
|
||||
opt("LOW", !formData.priority || String(formData.priority || "").toUpperCase() === "LOW", i18n.taskPriorityLow)
|
||||
), br(), br(),
|
||||
label(i18n.taskLocationLabel), br(),
|
||||
input({ type: "text", name: "location", value: editTask.location || "" }), br(),
|
||||
input({ type: "text", name: "location", value: formData.location || "" }), br(),
|
||||
label(i18n.taskTagsLabel), br(),
|
||||
input({ type: "text", name: "tags", value: editTags.join(", ") }), br(),
|
||||
label(i18n.taskVisibilityLabel), br(),
|
||||
select(
|
||||
{ name: "isPublic", id: "isPublic" },
|
||||
opt("PUBLIC", String(editTask.isPublic || "PUBLIC").toUpperCase() === "PUBLIC", i18n.taskPublic),
|
||||
opt("PRIVATE", String(editTask.isPublic || "").toUpperCase() === "PRIVATE", i18n.taskPrivate)
|
||||
opt("PUBLIC", String(formData.isPublic || "PUBLIC").toUpperCase() === "PUBLIC", i18n.taskPublic),
|
||||
opt("PRIVATE", String(formData.isPublic || "").toUpperCase() === "PRIVATE", i18n.taskPrivate)
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, currentFilter === "edit" ? i18n.taskUpdateButton : i18n.taskCreateButton)
|
||||
)
|
||||
|
|
@ -371,16 +325,16 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/tasks" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAssigned),
|
||||
button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterOpen),
|
||||
button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterInProgress),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterClosed),
|
||||
button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterLow),
|
||||
button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMedium),
|
||||
button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterHigh),
|
||||
button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterUrgent),
|
||||
button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAssigned).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterOpen).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterInProgress).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterClosed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterLow).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMedium).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterHigh).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterUrgent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.taskCreateButton)
|
||||
)
|
||||
);
|
||||
|
|
@ -421,15 +375,8 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => {
|
|||
: null;
|
||||
|
||||
const sideActions = [];
|
||||
if (task.author && task.author !== userId) {
|
||||
sideActions.push(form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: task.author }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
));
|
||||
}
|
||||
const assignNode = renderTaskAssignAction(task, isAssignedToMe, returnToSelf);
|
||||
if (assignNode) sideActions.push(assignNode);
|
||||
if (isAuthor) sideActions.push(...renderTaskOwnerActions(task, returnToSelf));
|
||||
const ownerActions = isAuthor ? renderTaskOwnerActions(task, returnToSelf) : [];
|
||||
|
||||
const infoRows = [];
|
||||
const pushRow = (labelText, valueNode) =>
|
||||
|
|
@ -444,19 +391,25 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => {
|
|||
if (task.location && String(task.location).trim()) pushRow(i18n.taskLocationLabel, task.location);
|
||||
|
||||
const taskSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(task.id, null, { spread: params.spreads || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, task.title)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(task.id, params.spreads)),
|
||||
task.image ? renderTaskMediaBlob(task.image, { class: "tribe-detail-image" }) : null,
|
||||
renderPhotoGallery(task, 'task'),
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
assignNode ? div({ class: "tribe-side-actions housing-status-row task-assign-row" }, ...assignNode) : null,
|
||||
isAuthor ? renderTaskStatusRow(task, returnToSelf) : null,
|
||||
tagsNode,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`)
|
||||
),
|
||||
assigneesListNode,
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null,
|
||||
ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null,
|
||||
renderDocumentActions('tasks', task.id)
|
||||
);
|
||||
|
||||
const returnToOpinions = `/tasks/${encodeURIComponent(task.id)}?filter=${encodeURIComponent(currentFilter)}`;
|
||||
|
|
@ -473,13 +426,13 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => {
|
|||
span({ class: "date-link" }, `${moment(task.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(task.author)
|
||||
),
|
||||
opinionsBar,
|
||||
renderTaskCommentsSection(task.id, comments, currentFilter)
|
||||
renderEngagement(task.id, opinionsBar, renderTaskCommentsSection(task.id, comments, currentFilter))
|
||||
);
|
||||
|
||||
return template(
|
||||
task.title,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(i18n.tasksTitle), p(i18n.tasksDescription)),
|
||||
filterBar,
|
||||
div({ class: "tribe-details" }, taskSide, taskMain)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ const {
|
|||
th,
|
||||
td
|
||||
} = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip } = require("./main_views");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
|
@ -49,21 +50,6 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const renderTorrentFavoriteToggle = (torrentObj, returnTo = "") =>
|
||||
form(
|
||||
{
|
||||
method: "POST",
|
||||
action: torrentObj.isFavorite
|
||||
? `/torrents/favorites/remove/${encodeURIComponent(torrentObj.key)}`
|
||||
: `/torrents/favorites/add/${encodeURIComponent(torrentObj.key)}`
|
||||
},
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button(
|
||||
{ type: "submit", class: "filter-btn" },
|
||||
torrentObj.isFavorite ? i18n.torrentRemoveFavoriteButton : i18n.torrentAddFavoriteButton
|
||||
)
|
||||
);
|
||||
|
||||
const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => {
|
||||
const returnTo = buildReturnTo(filter, params);
|
||||
const isAuthor = String(torrentObj.author) === String(userId);
|
||||
|
|
@ -93,63 +79,11 @@ const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => {
|
|||
};
|
||||
|
||||
const renderTorrentCommentsSection = (torrentId, comments = [], returnTo = null) => {
|
||||
const list = safeArr(comments);
|
||||
const commentsCount = list.length;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/torrents/${encodeURIComponent(torrentId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || "";
|
||||
const ts = c?.value?.timestamp || c?.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
const content = c?.value?.content || {};
|
||||
const rootId = content.fork || content.root || null;
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
);
|
||||
return renderSharedCommentsSection({
|
||||
action: `/torrents/${encodeURIComponent(torrentId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
|
|
@ -206,6 +140,7 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => {
|
|||
const tribeId = safeText(params.tribeId || "");
|
||||
return div(
|
||||
{ class: "div-center audio-form" },
|
||||
params.spreadWarning || null,
|
||||
form(
|
||||
{
|
||||
action: filter === "edit" ? `/torrents/update/${encodeURIComponent(torrentId)}` : "/torrents/create",
|
||||
|
|
@ -221,7 +156,7 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => {
|
|||
br(),
|
||||
span(i18n.torrentTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", placeholder: i18n.torrentTitlePlaceholder, value: torrentToEdit?.title || "", required: true }),
|
||||
input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.torrentTitlePlaceholder, value: torrentToEdit?.title || "", required: true }),
|
||||
br(),
|
||||
span(i18n.torrentDescriptionLabel),
|
||||
br(),
|
||||
|
|
@ -243,20 +178,8 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => {
|
|||
};
|
||||
|
||||
exports.torrentsView = async (torrents, filter = "all", torrentId = null, params = {}) => {
|
||||
const title =
|
||||
filter === "mine"
|
||||
? i18n.torrentMineSectionTitle
|
||||
: filter === "create"
|
||||
? i18n.torrentCreateSectionTitle
|
||||
: filter === "edit"
|
||||
? i18n.torrentUpdateSectionTitle
|
||||
: filter === "recent"
|
||||
? i18n.torrentRecentSectionTitle
|
||||
: filter === "top"
|
||||
? i18n.torrentTopSectionTitle
|
||||
: filter === "favorites"
|
||||
? i18n.torrentFavoritesSectionTitle
|
||||
: i18n.torrentAllSectionTitle;
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(torrentId) };
|
||||
const title = i18n.torrentsTitle;
|
||||
|
||||
const q = safeText(params.q || "");
|
||||
const sort = safeText(params.sort || "recent");
|
||||
|
|
@ -283,14 +206,14 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params
|
|||
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterTop),
|
||||
button(
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase()), button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.torrentFilterFavorites
|
||||
String(i18n.torrentFilterFavorites).toUpperCase()
|
||||
),
|
||||
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -315,9 +238,9 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params
|
|||
{ class: "filter-box__controls" },
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.torrentSortRecent),
|
||||
option({ value: "oldest", selected: sort === "oldest" }, i18n.torrentSortOldest),
|
||||
option({ value: "top", selected: sort === "top" }, i18n.torrentSortTop)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.torrentSortRecent),
|
||||
option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.torrentSortOldest),
|
||||
option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.torrentSortTop)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.torrentSearchButton)
|
||||
)
|
||||
|
|
@ -345,18 +268,21 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
|||
|
||||
const ownerActions = renderTorrentOwnerActions(filter, torrentObj, { q, sort });
|
||||
const sideActions = [];
|
||||
sideActions.push(renderTorrentFavoriteToggle(torrentObj, returnTo));
|
||||
if (torrentObj.author && String(torrentObj.author) !== String(userId)) {
|
||||
sideActions.push(form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: torrentObj.author }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
));
|
||||
}
|
||||
for (const a of ownerActions) sideActions.push(a);
|
||||
|
||||
const tagsNode = renderTags(torrentObj.tags);
|
||||
|
||||
const detailActions = div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(torrentObj.key, null, {
|
||||
author: torrentObj.author,
|
||||
favKind: 'torrents',
|
||||
isFavorite: torrentObj.isFavorite,
|
||||
spread: params.spreads || null,
|
||||
returnTo,
|
||||
reportTitle: torrentObj.title
|
||||
})
|
||||
);
|
||||
|
||||
const torrentSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
title ? h2({ class: "tribe-card-title" }, title) : null,
|
||||
|
|
@ -367,11 +293,11 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
|||
? p({ class: "tribe-side-description" }, ...renderUrl(torrentObj.description))
|
||||
: null,
|
||||
tagsNode,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(torrentObj.key, params.spreads)),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
);
|
||||
|
||||
const torrentMain = div({ class: "tribe-main" },
|
||||
detailActions,
|
||||
torrentObj.url && torrentObj.url.startsWith("&")
|
||||
? div({ class: "torrent-download" },
|
||||
a({ href: `/blob/${encodeURIComponent(torrentObj.url)}?name=${encodeURIComponent((torrentObj.title || 'download').replace(/\.torrent$/i, '') + '.torrent')}` , class: "filter-btn" }, i18n.torrentDownloadButton || "DOWNLOAD IT!")
|
||||
|
|
@ -384,18 +310,20 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(torrentObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
})(),
|
||||
renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants),
|
||||
renderTorrentCommentsSection(torrentObj.key, comments, returnTo)
|
||||
renderEngagement(torrentObj.key,
|
||||
renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants),
|
||||
renderTorrentCommentsSection(torrentObj.key, comments, returnTo)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
|
|
@ -411,14 +339,14 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
|||
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterTop),
|
||||
button(
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase()), button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.torrentFilterFavorites
|
||||
String(i18n.torrentFilterFavorites).toUpperCase()
|
||||
),
|
||||
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const filterButton = (mode, currentFilter) =>
|
|||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button(
|
||||
{ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' },
|
||||
i18n[mode + 'Button'] || mode
|
||||
String(i18n[mode + 'Button'] || mode).toUpperCase()
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -42,12 +42,11 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
)
|
||||
);
|
||||
} else if (c.type === 'image') {
|
||||
const { url, title, description, meme } = c;
|
||||
const { url, title, description } = c;
|
||||
contentHtml = div({ class: 'trending-image' },
|
||||
div({ class: 'card-section image' },
|
||||
title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, title)) : "",
|
||||
description ? [span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), p(...renderUrl(description))] : null,
|
||||
meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : "",
|
||||
div({ class: 'card-field' }, img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image' }))
|
||||
)
|
||||
);
|
||||
|
|
@ -129,7 +128,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
: ""
|
||||
)
|
||||
);
|
||||
} else if (c.type === 'votes') {
|
||||
} else if (c.type === 'votes' || c.type === 'poll') {
|
||||
const { question, deadline, votes: vmap, totalVotes } = c;
|
||||
const votesList = vmap && typeof vmap === 'object'
|
||||
? Object.entries(vmap).map(([o, cnt]) => ({ option: o, count: cnt }))
|
||||
|
|
@ -198,12 +197,11 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase())
|
||||
),
|
||||
renderContentActions(item.key, detailHref)
|
||||
renderContentActions(item.key, detailHref, { spread: spreadMap.get(item.key) || null })
|
||||
),
|
||||
div(
|
||||
{ class: 'card-section trending-card-body' },
|
||||
contentHtml,
|
||||
div({ class: 'card-spread-left' }, renderSpreadButton(item.key, spreadMap.get(item.key))),
|
||||
p(
|
||||
{ class: 'card-footer' },
|
||||
span({ class: 'date-link' }, `${created} ${i18n.performed} `),
|
||||
|
|
@ -228,14 +226,16 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
);
|
||||
})(),
|
||||
details({ class: 'opinions-voting-collapse' },
|
||||
summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`),
|
||||
summary({ class: 'opinions-summary' },
|
||||
span({ class: 'opinions-summary-icon' }, 'ꔍ'),
|
||||
span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)),
|
||||
div(
|
||||
{ class: 'voting-buttons' },
|
||||
categories.map(cat =>
|
||||
form({ method: 'POST', action: `/trending/${encodeURIComponent(item.key)}/${cat}` },
|
||||
button(
|
||||
{ class: 'vote-btn' },
|
||||
`${voteLabelFor(cat)} [${c.opinions?.[cat] || 0}]`
|
||||
`${String(voteLabelFor(cat)).toUpperCase()} [${c.opinions?.[cat] || 0}]`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -245,14 +245,14 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
);
|
||||
};
|
||||
|
||||
exports.trendingView = (items, filter, categories = opinionCategories, spreadMap = new Map()) => {
|
||||
exports.trendingView = (items, filter, categories = opinionCategories, spreadMap = new Map(), q = '') => {
|
||||
const seenDocumentTitles = new Set();
|
||||
const title = i18n.trendingTitle;
|
||||
|
||||
const baseFilters = ['TOP', 'ALL', 'MINE', 'RECENT'];
|
||||
const baseFilters = ['ALL', 'MINE', 'RECENT', 'TOP'];
|
||||
const contentFilters = [
|
||||
['feed', 'votes', 'event', 'task', 'report'],
|
||||
['project', 'industry', 'transfer', 'shopProduct'],
|
||||
['votes', 'event', 'task', 'report'],
|
||||
['feed', 'project', 'industry', 'shopProduct', 'transfer'],
|
||||
['audio', 'bookmark', 'document', 'image', 'torrent', 'video']
|
||||
];
|
||||
|
||||
|
|
@ -273,7 +273,9 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap
|
|||
return bVotes !== aVotes ? bVotes - aVotes : b.value.timestamp - a.value.timestamp;
|
||||
});
|
||||
} else if (contentFilters.flat().includes(filter)) {
|
||||
filteredItems = filteredItems.filter(item => item.value.content.type === filter);
|
||||
filteredItems = filteredItems.filter(item =>
|
||||
item.value.content.type === filter ||
|
||||
(filter === 'votes' && item.value.content.type === 'poll'));
|
||||
} else if (filter !== 'ALL') {
|
||||
filteredItems = filteredItems.filter(item => (item.value.content.opinions_inhabitants || []).length > 0);
|
||||
}
|
||||
|
|
@ -308,9 +310,18 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap
|
|||
div({ class: 'column' }, row.map(mode => filterButton(mode, filter)))
|
||||
)
|
||||
),
|
||||
div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/trending', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({ type: 'text', name: 'q', value: q, placeholder: i18n.trendingSearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
cards.length
|
||||
? div({ class: 'trending-container', style: 'display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;' }, ...cards)
|
||||
? div({ class: 'trending-container' }, ...cards)
|
||||
: div({ class: 'no-results' }, p(i18n.trendingNoContentMessage))
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ const {
|
|||
select,
|
||||
option
|
||||
} = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
const { renderMapLocationVisitLabel } = require("./maps_view");
|
||||
|
|
@ -37,18 +38,6 @@ const buildReturnTo = (filter, params = {}) => {
|
|||
return `/videos?${parts.join("&")}`;
|
||||
};
|
||||
|
||||
const renderPMButton = (recipient, className = "filter-btn") => {
|
||||
const r = safeText(recipient);
|
||||
if (!r) return null;
|
||||
if (String(r) === String(userId)) return null;
|
||||
|
||||
return form(
|
||||
{ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: r }),
|
||||
button({ type: "submit", class: className }, i18n.privateMessage)
|
||||
);
|
||||
};
|
||||
|
||||
const renderTags = (tags) => {
|
||||
const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean);
|
||||
return list.length
|
||||
|
|
@ -59,25 +48,10 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const renderVideoFavoriteToggle = (videoObj, returnTo = "") =>
|
||||
form(
|
||||
{
|
||||
method: "POST",
|
||||
action: videoObj.isFavorite
|
||||
? `/videos/favorites/remove/${encodeURIComponent(videoObj.key)}`
|
||||
: `/videos/favorites/add/${encodeURIComponent(videoObj.key)}`
|
||||
},
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button(
|
||||
{ type: "submit", class: "filter-btn" },
|
||||
videoObj.isFavorite ? i18n.videoRemoveFavoriteButton : i18n.videoAddFavoriteButton
|
||||
)
|
||||
);
|
||||
|
||||
const renderVideoPlayer = (videoObj) =>
|
||||
videoObj?.url
|
||||
? div(
|
||||
{ class: "video-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" },
|
||||
{ class: "video-container video-container-row" },
|
||||
videoHyperaxe({
|
||||
controls: true,
|
||||
src: `/blob/${encodeURIComponent(videoObj.url)}`,
|
||||
|
|
@ -115,66 +89,11 @@ const renderVideoOwnerActions = (filter, videoObj, params = {}) => {
|
|||
};
|
||||
|
||||
const renderVideoCommentsSection = (videoId, comments = [], returnTo = null) => {
|
||||
const list = safeArr(comments).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
return renderSharedCommentsSection({
|
||||
action: `/videos/${encodeURIComponent(videoId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
const commentsCount = list.length;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/videos/${encodeURIComponent(videoId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || "";
|
||||
const ts = c?.value?.timestamp || c?.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
const content = c?.value?.content || {};
|
||||
const rootId = content.fork || content.root || null;
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
);
|
||||
};
|
||||
|
||||
const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) => {
|
||||
|
|
@ -184,7 +103,6 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {})
|
|||
? videos.map((videoObj) => {
|
||||
const commentCount = typeof videoObj.commentCount === "number" ? videoObj.commentCount : 0;
|
||||
const title = safeText(videoObj.title);
|
||||
const ownerActions = renderVideoOwnerActions(filter, videoObj, params);
|
||||
|
||||
const isOwn = videoObj.author && String(videoObj.author) === String(userId);
|
||||
return div(
|
||||
|
|
@ -192,49 +110,17 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {})
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`)
|
||||
renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads || null, author: videoObj.author, favKind: 'videos', isFavorite: videoObj.isFavorite, reportTitle: videoObj.title })
|
||||
),
|
||||
div(
|
||||
{ class: "card-section video-card-body" },
|
||||
div(
|
||||
{ class: "bookmark-topbar" },
|
||||
div(
|
||||
{ class: "bookmark-topbar-left" },
|
||||
renderVideoFavoriteToggle(videoObj, returnTo),
|
||||
renderPMButton(videoObj.author)
|
||||
),
|
||||
ownerActions.length
|
||||
? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions))
|
||||
: null
|
||||
),
|
||||
title ? h2(title) : null,
|
||||
videoObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(videoObj.lifetime, i18n)) : null,
|
||||
renderVideoPlayer(videoObj),
|
||||
div(
|
||||
{ class: "card-comments-summary" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ":"),
|
||||
span({ class: "card-value" }, String(commentCount)),
|
||||
br(),
|
||||
br(),
|
||||
form(
|
||||
{ method: "GET", action: `/videos/${encodeURIComponent(videoObj.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "filter", value: filter || "all" }),
|
||||
params.q ? input({ type: "hidden", name: "q", value: params.q }) : null,
|
||||
params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null,
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton)
|
||||
)
|
||||
renderEngagement(videoObj.key,
|
||||
renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants),
|
||||
renderCommentsLink({ href: `/videos/${encodeURIComponent(videoObj.key)}`, count: commentCount })
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(Object.values(videoObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)))
|
||||
),
|
||||
renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants)
|
||||
),
|
||||
div({ class: "card-spread-left" }, renderSpreadButton(videoObj.key, (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads)),
|
||||
renderMapLocationVisitLabel(videoObj.mapUrl),
|
||||
br(),
|
||||
(() => {
|
||||
|
|
@ -244,12 +130,12 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {})
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(videoObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
|
@ -265,6 +151,7 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => {
|
|||
|
||||
return div(
|
||||
{ class: "div-center video-form" },
|
||||
params.spreadWarning || null,
|
||||
form(
|
||||
{
|
||||
action: filter === "edit" ? `/videos/update/${encodeURIComponent(videoId)}` : "/videos/create",
|
||||
|
|
@ -279,7 +166,7 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => {
|
|||
br(),
|
||||
span(i18n.videoTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || "" }),
|
||||
input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || "" }),
|
||||
br(),
|
||||
span(i18n.videoDescriptionLabel),
|
||||
br(),
|
||||
|
|
@ -305,20 +192,8 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => {
|
|||
};
|
||||
|
||||
exports.videoView = async (videos, filter = "all", videoId = null, params = {}) => {
|
||||
const title =
|
||||
filter === "mine"
|
||||
? i18n.videoMineSectionTitle
|
||||
: filter === "create"
|
||||
? i18n.videoCreateSectionTitle
|
||||
: filter === "edit"
|
||||
? i18n.videoUpdateSectionTitle
|
||||
: filter === "recent"
|
||||
? i18n.videoRecentSectionTitle
|
||||
: filter === "top"
|
||||
? i18n.videoTopSectionTitle
|
||||
: filter === "favorites"
|
||||
? i18n.videoFavoritesSectionTitle
|
||||
: i18n.videoAllSectionTitle;
|
||||
if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(videoId) };
|
||||
const title = i18n.videoTitle;
|
||||
|
||||
const q = safeText(params.q || "");
|
||||
const sort = safeText(params.sort || "recent");
|
||||
|
|
@ -345,14 +220,14 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {})
|
|||
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterTop),
|
||||
button(
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase()), button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.videoFilterFavorites
|
||||
String(i18n.videoFilterFavorites).toUpperCase()
|
||||
),
|
||||
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -377,9 +252,9 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {})
|
|||
{ class: "filter-box__controls" },
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.videoSortRecent),
|
||||
option({ value: "oldest", selected: sort === "oldest" }, i18n.videoSortOldest),
|
||||
option({ value: "top", selected: sort === "top" }, i18n.videoSortTop)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.videoSortRecent),
|
||||
option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.videoSortOldest),
|
||||
option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.videoSortTop)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.videoSearchButton)
|
||||
)
|
||||
|
|
@ -407,14 +282,21 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params
|
|||
|
||||
const ownerActions = renderVideoOwnerActions(filter, videoObj, { q, sort });
|
||||
const sideActions = [];
|
||||
sideActions.push(renderVideoFavoriteToggle(videoObj, returnTo));
|
||||
if (videoObj.author && String(videoObj.author) !== String(userId)) {
|
||||
sideActions.push(renderPMButton(videoObj.author));
|
||||
}
|
||||
for (const a of ownerActions) sideActions.push(a);
|
||||
|
||||
const tagsNode = renderTags(videoObj.tags);
|
||||
|
||||
const detailActions = div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(videoObj.key, null, {
|
||||
author: videoObj.author,
|
||||
favKind: 'videos',
|
||||
isFavorite: videoObj.isFavorite,
|
||||
spread: params.spreads || null,
|
||||
returnTo,
|
||||
reportTitle: videoObj.title
|
||||
})
|
||||
);
|
||||
|
||||
const videoSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
title ? h2({ class: "tribe-card-title" }, title) : null,
|
||||
|
|
@ -425,12 +307,12 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params
|
|||
? p({ class: "tribe-side-description" }, ...renderUrl(videoObj.description))
|
||||
: null,
|
||||
tagsNode,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(videoObj.key, params.spreads)),
|
||||
renderMapLocationVisitLabel(videoObj.mapUrl),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
);
|
||||
|
||||
const videoMain = div({ class: "tribe-main" },
|
||||
detailActions,
|
||||
renderVideoPlayer(videoObj),
|
||||
(() => {
|
||||
const createdTs = videoObj.createdAt ? new Date(videoObj.createdAt).getTime() : NaN;
|
||||
|
|
@ -439,18 +321,20 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params
|
|||
|
||||
return p(
|
||||
{ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(videoObj.author),
|
||||
showUpdated
|
||||
? span(
|
||||
{ class: "votations-comment-date" },
|
||||
` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`
|
||||
` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}`
|
||||
)
|
||||
: null
|
||||
);
|
||||
})(),
|
||||
renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants),
|
||||
renderVideoCommentsSection(videoObj.key, comments, returnTo)
|
||||
renderEngagement(videoObj.key,
|
||||
renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants),
|
||||
renderVideoCommentsSection(videoObj.key, comments, returnTo)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
|
|
@ -466,14 +350,14 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params
|
|||
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterTop),
|
||||
button(
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase()), button(
|
||||
{ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" },
|
||||
i18n.videoFilterFavorites
|
||||
String(i18n.videoFilterFavorites).toUpperCase()
|
||||
),
|
||||
|
||||
button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue