From 7eeac8b004dce9ec62d49164b7416713121fd10f Mon Sep 17 00:00:00 2001 From: SITO Date: Tue, 18 Aug 2026 22:49:21 +0200 Subject: [PATCH] upstream: integrar 0.9.5 en style.css y las vistas grandes style.css pasa a 0.9.5: lo unico que la rama le habia hecho era borrar reglas de housing, y su estilo propio vive en OasisMobile.css, que carga despues y gana igual. Se adopta 0.9.5 en tags_view (anade busqueda y filtros mine/recent), vote_view, market_view y activity_view, donde la rama solo arrastraba la version anterior: renderContentActions de dos parametros, comentarios de mercado propios y un import muerto de renderHiveNav que no se llegaba a llamar. Quedan a proposito en la version de la rama forum_view y chats_view: llevan funcionalidad movil propia (la tira de temas del foro y la interfaz de responder mensajes, que acompana al replyTo del modelo) entrelazada con estructuras que 0.9.5 ha reescrito. Integrarlas pide rehacer esa funcionalidad sobre la forma nueva, y no es un cambio que convenga colar entre otros veinte. Verificado con la app en marcha: 21 rutas OK, 0 claves i18n rotas, 0 requires rotos, interfaz intacta y orden de hojas correcto (style.css, mobile.css, OasisMobile.css). --- src/client/assets/styles/style.css | 1513 +++++++++++++++++++++++----- src/views/activity_view.js | 417 +++++--- src/views/market_view.js | 240 ++--- src/views/tags_view.js | 42 +- src/views/vote_view.js | 153 ++- 5 files changed, 1722 insertions(+), 643 deletions(-) diff --git a/src/client/assets/styles/style.css b/src/client/assets/styles/style.css index caaeb7c..1382cff 100644 --- a/src/client/assets/styles/style.css +++ b/src/client/assets/styles/style.css @@ -80,12 +80,6 @@ a:hover { .update-banner-icon { font-size: 1.15em; - animation: update-spin 2.5s linear infinite; -} - -@keyframes update-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } } .update-banner-text { @@ -988,7 +982,7 @@ article img { .feed-container .post-image{max-width:100%;width:auto;max-height:480px;height:auto} .feed-container .post-video,.feed-container .video-container video{width:100%;max-width:720px;max-height:480px;height:auto} -.feed-container .post-audio,.feed-container .audio-container audio{width:100%;max-width:640px} +.feed-container .post-audio,.feed-container .audio-container audio{width:100%} .fediverse-network{border-radius:8px;padding:18px 22px;margin-top:10px;display:flex;flex-direction:column;gap:16px} .fediverse-network h3{margin:0} @@ -1135,14 +1129,84 @@ button.create-button:hover { } .filter-box { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; padding: 16px 18px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(0, 0, 0, 0.18); } +.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), +.filter-box select, +.filter-box button { + height: 42px !important; + box-sizing: border-box !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin: 0 !important; + vertical-align: middle; +} + +div:has(> .filter-box) { + border: none !important; + background: transparent !important; + padding: 0 !important; + box-shadow: none !important; +} + +.filter-box div { + border: none !important; + background: transparent !important; + box-shadow: none !important; + padding: 0 !important; +} + +.filter-box__controls .filter-box__input, +.filter-box__controls input[type="text"], +.filter-box__controls input[type="number"], +.filter-box__controls input[type="date"], +.filter-box__controls input[type="datetime-local"], +.filter-box__controls select { + width: auto; + flex: 0 1 220px; + min-width: 140px; +} + +.filter-box .transfer-range, +.filter-box__controls .transfer-range { + display: flex; + gap: 8px; + flex: 1 1 100% !important; +} + +.filter-box select { + flex: 1 1 100% !important; + max-width: 340px; +} + +.filter-box .filter-box__button, +.filter-box button { + flex: 0 0 auto; +} + +.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]), +.filter-box select { + box-sizing: border-box; + height: 42px; + padding: 10px 12px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.12); + color: #ffd700; + outline: none; +} + .filter-box__input { width: 100%; + flex: 1 1 100%; box-sizing: border-box; height: 42px; padding: 10px 12px; @@ -1161,15 +1225,16 @@ button.create-button:hover { display: flex; gap: 12px; align-items: center; - margin-top: 14px; + margin-top: 0; + flex: 1 1 100%; flex-wrap: wrap; } .filter-box__select { box-sizing: border-box; - height: 36px; + height: 42px; padding: 6px 10px; - border-radius: 6px; + border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.12); color: inherit; @@ -1177,14 +1242,26 @@ button.create-button:hover { } .filter-box__button { - height: 36px; - padding: 0 14px; - border-radius: 6px; + height: 42px; + padding: 0 18px; + border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.16); color: #ffd700; } +.filter-box__controls input:not([type="hidden"]), +.filter-box__controls select, +.filter-box__controls button { + height: 42px !important; + box-sizing: border-box !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin: 0 !important; + align-self: center !important; + line-height: normal; +} + .bookmark-topbar { display: flex; gap: 8px; @@ -1518,18 +1595,57 @@ button.create-button:hover { cursor: pointer; font: inherit; padding: 10px 20px; - border: 1px solid #ff3300; + border: 1px solid rgba(128, 128, 128, .5) !important; border-radius: 5px; - background-color: #ff6600; - color: #fff; + background: rgba(128, 128, 128, .12) !important; transition: background-color .2s, border-color .2s; list-style: none; } -.opinions-summary:hover { background-color: #ff9900; border-color: #e68a00; color: #000; } +.opinions-summary:hover { background: rgba(128, 128, 128, .25) !important; } .opinions-summary::-webkit-details-marker { display: none; } .opinions-summary::after { content: " \25BE"; opacity: 0.85; } details[open] > .opinions-summary::after { content: " \25B4"; } +.comments-collapse { margin: 8px 0; } +.comments-summary { + display: inline-block !important; + width: auto !important; + max-width: max-content; + box-sizing: border-box; + cursor: pointer; + font: inherit; + padding: 10px 20px; + border: 1px solid rgba(128, 128, 128, .5); + border-radius: 5px; + background: rgba(128, 128, 128, .12) !important; + list-style: none; +} +.comments-summary:hover { background: rgba(128, 128, 128, .25) !important; } +.comments-summary::-webkit-details-marker { display: none; } +.comments-summary::after { content: " \25BE"; opacity: 0.85; } +details[open] > .comments-summary::after { content: " \25B4"; } +.comments-summary-icon { margin-right: 6px; } + +.comments-link { + text-decoration: none; + text-align: center; +} + +.comments-link::after { content: ""; } + +.engage-on { + border-color: #e68a00 !important; + background: #ff9900 !important; + color: #000 !important; +} + +.engage-on:hover { + background: #ffb733 !important; + border-color: #ff9900 !important; + color: #000 !important; +} +.opinions-summary-icon { margin-right: 6px; } + .message-banner { margin: 12px 0; padding: 12px 16px; @@ -1539,27 +1655,6 @@ details[open] > .opinions-summary::after { content: " \25B4"; } } .message-banner p { margin: 0; } -.feed-search-form { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.feed-search-input { - flex: 1 1 260px; - min-width: 220px; - height: 40px; - box-sizing: border-box; -} - -.feed-search-btn { - height: 40px; - display: inline-flex; - align-items: center; - box-sizing: border-box; -} - .type-label { font-weight: bold; margin-top: 0.5em; @@ -1818,13 +1913,19 @@ background:#ffcc00; color:#000; } .buy-btn { - display: inline-block; - background: #ffcc00; + display: block; + width: 100%; + padding: 14px 20px; + font-size: 1.2em; + font-weight: bold; + background: #ffa300; color: #000; text-decoration: none; text-align: center; + border-radius: 8px; } .buy-btn:hover { background: #ffcc00; color: #000; } +form:has(> .buy-btn) { width: 100%; } .gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:10px; padding:10px; @@ -1922,6 +2023,9 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border-radius: 6px; } +.zoom-link{display:inline-block;border:none !important;background:transparent !important;padding:0 !important} +.zoom-link img{cursor:zoom-in} + .lightbox-close { position: absolute; top: 15px; @@ -2102,6 +2206,38 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 10px; } +.doc-export-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + margin-top: 12px; + border: none !important; + background: transparent !important; +} + +.doc-export-actions form { + border: none !important; + background: transparent !important; + margin: 0; +} + +.vote-outcome { + font-weight: bold; +} + +.vote-outcome-yes { + color: #4caf50 !important; +} + +.vote-outcome-no { + color: #e53935 !important; +} + +.vote-outcome-pending { + color: #ffcc00 !important; +} + .event-actions { display: flex; gap: 10px; @@ -2185,6 +2321,24 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 12px; } +.cv-threshold-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.cv-card-fields { + display: flex; + flex-direction: column; + gap: 4px; + margin: 6px 0; +} + +.cv-threshold-row label { + margin: 0; +} + .card-salary { display: inline-block; font-weight: 700; @@ -2192,6 +2346,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; letter-spacing: 0.02em; } +.transfer-amount-centered { + display: block; + text-align: center; +} + .mode-buttons { display: flex; gap: 10px; @@ -2493,6 +2652,8 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border: 0 !important; padding: 0 !important; width: 100%; + align-items: center; + justify-content: center; } .inhabitant-left .cv-actions form, @@ -2500,11 +2661,42 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; background: transparent !important; border: 0 !important; padding: 0 !important; - width: 100%; + width: auto; } -.inhabitant-left .cv-actions button.btn { +.inhabitant-user-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; width: 100%; + margin-top: 10px; + padding: 10px !important; + border: 1px solid rgba(128, 128, 128, .25) !important; + border-radius: 8px; + background: transparent !important; +} + +.inhabitant-user-actions form { + background: transparent !important; + border: 0 !important; + padding: 0 !important; + margin: 0; +} + +.inhabitant-relationship { + background: transparent !important; + border: 0 !important; + padding: 0 !important; +} + +.inhabitant-relationship .relationship-status { + justify-content: center; + text-align: center; +} + +.inhabitant-relationship .relationship-actions { + justify-content: center; } .inhabitant-left .cv-actions p { @@ -2542,6 +2734,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; + justify-content: center; } .status { @@ -2604,11 +2797,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; gap: 1.1em; background: none; } -.visit-forum-form { - display: flex; - justify-content: center; - margin-top: 10px; -} .forum-score-col { min-width: 78px; display: flex; @@ -2898,6 +3086,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .comment-textarea { resize: vertical; width: 100%; + box-sizing: border-box; padding: 8px; background: #1e1e1e; color: #ffd740; @@ -2997,6 +3186,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; font-weight: bold; } +.forum-negative-votes { + font-weight: bold; + margin-left: 12px; +} + .forum-messages { font-weight: 500; } @@ -3434,7 +3628,8 @@ a.pm-btn:hover { text-decoration: none } display: flex; align-items: center; gap: 10px; - flex-wrap: nowrap; + flex-wrap: wrap; + max-width: 100%; } .project-control-select, @@ -3445,7 +3640,10 @@ a.pm-btn:hover { text-decoration: none } .project-control-select, .project-control-input { - min-width: 220px; + min-width: 0; + flex: 1 1 160px; + max-width: 100%; + box-sizing: border-box; } .project-control-btn { @@ -3460,7 +3658,7 @@ a.pm-btn:hover { text-decoration: none } @media (max-width: 820px) { .project-control-select, .project-control-input { - min-width: 180px; + flex-basis: 100%; } .project-progress-input { @@ -3640,7 +3838,7 @@ a.pm-btn:hover { text-decoration: none } .melody-player-card{border:1px solid #2a2a2a;border-radius:8px;padding:16px;background:#111;margin:16px 0} .melody-meta{margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:6px} .melody-meta-sep{color:#555} -.melody-audio{width:100%;max-width:520px} +.melody-audio{width:100%} .melody-section-title{font-size:16px;color:#FFA500;margin:16px 0 8px 0} .melody-help{color:#888;font-size:13px;margin:0 0 12px 0} .melody-notes-grid{display:flex;flex-wrap:wrap;gap:6px} @@ -3686,7 +3884,8 @@ a.pm-btn:hover { text-decoration: none } .melody-upload-submit{width:auto;display:inline-block;padding:6px 18px;align-self:flex-start} .melody-upload-label{color:#FFD700;font-size:13px;font-weight:600;display:inline-block;margin-top:6px} .melody-bcs-list{display:flex;flex-direction:column;gap:14px;margin-top:14px;background:transparent !important;border:0 !important;padding:0 !important} -.audio-container{display:flex;gap:8px;align-items:center;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important} +.audio-container{display:flex;flex-direction:column;gap:8px;align-items:stretch;width:100%;background:transparent !important;border:0 !important;padding:0 !important} +.audio-container audio{width:100%} .audio-transcode-form{display:inline-block;background:transparent !important;border:0 !important;padding:0 !important;margin:0} .melody-bcs-card{background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} .melody-bcs-head{display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important;padding:0 !important} @@ -3733,7 +3932,7 @@ a.pm-btn:hover { text-decoration: none } .shop-detail-media img{max-width:100%;border-radius:6px} .shop-detail-desc{margin:8px 0} .shop-share{margin:8px 0;display:flex;align-items:center;gap:8px} -.shop-share-input{flex:1;background:#222;border:1px solid #555;color:#ccc;padding:6px 10px;border-radius:4px;font-family:monospace;font-size:13px} +.shop-share-input{flex:1;background:rgba(128,128,128,.12);border:1px solid rgba(255,163,0,.6);color:#ffa300;font-weight:bold;padding:6px 10px;border-radius:4px;font-family:monospace;font-size:13px;margin:0} .shop-products-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin:12px 0} .shop-product-card{border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .shop-product-media{max-height:180px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#111} @@ -3760,13 +3959,14 @@ a.pm-btn:hover { text-decoration: none } /* carbon footprint chart */ .carbon-section-title{font-size:18px;color:#555;margin:8px 0;font-weight:600} +#carbon{scroll-margin-top:16px} .carbon-chart{margin:12px 0;background:transparent !important;border:none !important} .carbon-bar-label{display:flex;justify-content:space-between;font-size:14px;margin:4px 0 2px;background:transparent !important;border:none !important} .carbon-bar-track{background:#3a3a3a !important;border-radius:6px;height:22px;overflow:hidden;border:none !important} .carbon-bar-fill{height:100%;border-radius:6px;min-width:2px;transition:width 0.4s ease;border:none !important} .carbon-bar-network{background:#2ecc71 !important} .carbon-bar-mine{background:#3498db !important} -.carbon-bar-max{background:#555 !important;border:none !important} +.carbon-bar-max{background:#ffa300 !important;border:none !important} .carbon-bar-note{font-size:13px;color:#ffd700;margin:6px 0 2px} .carbon-bar-formula{font-size:12px;color:#ffd700;margin:2px 0 10px} @@ -3997,6 +4197,16 @@ a.pm-btn:hover { text-decoration: none } .tribe-card-title { margin: 0; font-size: 1.15em; + overflow-wrap: anywhere; +} + +.jobs-grid .tribe-card-title, +.tribe-grid .tribe-card-title, +.trending-card .tribe-card-title { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } .tribe-card-footer { @@ -4063,7 +4273,26 @@ a.pm-btn:hover { text-decoration: none } .tribe-side-actions select, .tribe-side-actions input[type="number"], .tribe-side-actions input[type="text"] { height: 36px; box-sizing: border-box } -.tribe-side-actions button { height: 36px; box-sizing: border-box; display: inline-flex; align-items: center } +.tribe-side-actions button, +.tribe-side-actions > a, +.tribe-side-actions form > a { + height: 36px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.tribe-side-actions a > button { + height: auto; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + letter-spacing: inherit; + text-transform: inherit; +} .tribe-side-subtribes { margin: 8px 0; @@ -4234,59 +4463,30 @@ a.pm-btn:hover { text-decoration: none } color: #ffa300; transition: background 0.15s, color 0.15s; } -.tribe-action-btn, -.tribe-side-actions button, -.tribe-side-actions a, -.map-detail-actions button, -.map-detail-actions a { - display: inline-block; - padding: 6px 16px; - border-radius: 6px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.5px; - line-height: 1.2; - text-transform: uppercase; - text-decoration: none; - text-align: center; - cursor: pointer; - border: 1px solid #ffa300; - background: transparent; - color: #ffa300; - transition: background 0.15s, color 0.15s; -} -.tribe-side-actions button:hover, -.tribe-side-actions a:hover, -.map-detail-actions button:hover, -.map-detail-actions a:hover { - background: #ffa300; - color: #000; -} -.tribe-side-actions .tribe-action-btn.danger-btn, -.tribe-side-actions .delete-btn, -.map-detail-actions .delete-btn { - border-color: #d9534f; - color: #d9534f; -} -.tribe-side-actions .tribe-action-btn.danger-btn:hover, -.tribe-side-actions .delete-btn:hover, -.map-detail-actions .delete-btn:hover { - background: #d9534f; - color: #000; -} .tribe-action-btn.danger-btn, .tribe-card-actions .tribe-action-btn.danger-btn, -.tribe-side-actions .tribe-action-btn.danger-btn { +.tribe-side-actions .tribe-action-btn.danger-btn, +.tribe-side-actions .delete-btn { border: 1px solid #d9534f !important; color: #d9534f !important; background: transparent !important; } .tribe-action-btn.danger-btn:hover, .tribe-card-actions .tribe-action-btn.danger-btn:hover, -.tribe-side-actions .tribe-action-btn.danger-btn:hover { +.tribe-side-actions .tribe-action-btn.danger-btn:hover, +.tribe-side-actions .delete-btn:hover { background: #d9534f !important; color: #000 !important; } +.tribe-side-actions .update-btn { + border: 1px solid #ffa300; + color: #ffa300; + background: transparent; +} +.tribe-side-actions .update-btn:hover { + background: #ffa300; + color: #000; +} .tribe-action-btn:hover { background: #ffa300; color: #000; @@ -4296,9 +4496,8 @@ a.pm-btn:hover { text-decoration: none } .calendar-interval-block { margin: 8px 0 16px; } .calendar-interval-label { display: block; margin-bottom: 6px; } -.calendar-interval-until { margin-top: 10px; } -.calendar-interval-row { display: flex; gap: 16px; flex-wrap: wrap; } -.calendar-interval-option { display: flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; } +.calendar-interval-until { margin-top: 14px; } +.calendar-interval-select { margin: 0; } .tribe-thumb-grid { display: grid; @@ -4895,14 +5094,6 @@ a.pm-btn:hover { text-decoration: none } margin-bottom: 16px; } -.map-card { - background-color: #222; - border: 1px solid #444; - border-radius: 8px; - padding: 16px; - margin-bottom: 16px; -} - .map-card-info { display: flex; gap: 12px; @@ -4953,14 +5144,6 @@ a.pm-btn:hover { text-decoration: none } word-break: break-all; } -.map-detail-info { - display: flex; - gap: 16px; - align-items: center; - flex-wrap: wrap; - margin-bottom: 12px; -} - .map-wrap { margin-right: 30px; position: relative; @@ -5021,10 +5204,6 @@ a.pm-btn:hover { text-decoration: none } font-size: 1.2rem; } -.map-create-layout { - width: 100%; -} - .map-form { margin-top: 12px; } @@ -5048,86 +5227,29 @@ a.pm-btn:hover { text-decoration: none } display: flex; border: 0px; gap: 12px; + align-items: center; +} + +.map-form-row .filter-btn { + height: 36px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + text-decoration: none; } .map-form-field { flex: 1; } -.map-card { - background-color: #222; - border: 1px solid #444; - border-radius: 8px; - padding: 12px 16px; - margin-bottom: 12px; - display: flex; - gap: 16px; - align-items: flex-start; -} - -.map-card-thumb-link { - flex-shrink: 0; -} - -.map-card-thumb { - width: 120px !important; - height: 120px !important; - max-width: none !important; - border-radius: 6px !important; - border: 1px solid #555 !important; - object-fit: cover; -} - -.map-card-body { - flex: 1; - min-width: 0; -} - -.map-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px; -} - -.map-card-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - align-items: center; -} - .map-click-hint { color: #FFA500; font-size: 0.85rem; padding: 6px 0; } -.map-detail { - background-color: #222; - border: 1px solid #444; - border-radius: 8px; - padding: 16px; -} - -.map-detail-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - flex-wrap: wrap; - margin-bottom: 12px; -} - -.map-detail-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - align-items: center; -} - .map-marker-form { margin-top: 20px; padding-top: 16px; @@ -5239,6 +5361,31 @@ a.pm-btn:hover { text-decoration: none } font-size: 0.9rem; } +.search-card { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.search-card .card-value, +.search-card .styled-text, +.search-card p { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.search-card img, +.search-card video { + max-width: 100%; + height: auto; +} + +.search-card audio { + width: 100%; + max-width: 100%; +} + .search-result-image { max-width: 100%; max-height: 300px; @@ -5273,12 +5420,6 @@ a.pm-btn:hover { text-decoration: none } margin: 16px 0; } -.maps-list { - display: flex; - flex-direction: column; - gap: 16px; -} - .maps-search { margin-bottom: 16px; } @@ -5340,12 +5481,6 @@ a.pm-btn:hover { text-decoration: none } .map-markers-list h3 { margin-bottom: 4px; } -.map-form-full { - width: 100%; -} -.map-form-full form { - width: 100%; -} .map-osm-embed { width: 100%; height: 450px; @@ -5359,10 +5494,6 @@ a.pm-btn:hover { text-decoration: none } overflow-y: hidden; border: 0px; } -.map-form-full, .map-form-full form { - box-sizing: border-box; - overflow: hidden; -} .map-popup { display: none; position: fixed; @@ -5479,14 +5610,37 @@ a.pm-btn:hover { text-decoration: none } .chat-participants-list li{padding:4px 0;font-size:0.85rem} .chat-message-form{background:#1a1a1a;border:1px solid #333;border-radius:4px;padding:10px} .chat-message-form textarea{width:100%;box-sizing:border-box;background:#111;color:#eee;border:1px solid #555;border-radius:4px;padding:8px;resize:vertical;font-family:inherit} -.chat-messages-list{display:flex;flex-direction:column;gap:4px} -.chat-message{padding:6px 10px;border-radius:4px;background:#1a1a1a;word-break:break-word;border:none} -.chat-message-author{border-left-color:#FFA500;background:#1f1800} -.chat-message-self{border-left-color:#888;background:#181818} -.chat-message-meta{font-size:0.8rem;color:#888;border:none} -.chat-message-date{color:#666} -.chat-message-sender a{color:#aaa} -.chat-message-text{display:block;margin-top:2px;font-size:0.9rem;white-space:pre-wrap;word-break:break-word} +.main-content.chatsux-only .main-column{max-width:100%;width:100%} +.chat-workspace{display:flex;gap:14px;align-items:stretch;min-height:72vh} +.chat-topics{width:280px;flex-shrink:0;display:flex;flex-direction:column;gap:4px;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:8px;max-height:82vh;overflow-y:auto} +.chat-topics-head{display:flex;justify-content:space-between;align-items:center;font-weight:bold;padding:4px 6px;border:none !important;background:transparent !important} +.chat-topics-new{padding:2px 10px;line-height:1.3} +.chat-topics-title-link{font-weight:bold;color:#ffa300;text-decoration:none} +.chat-topics-title-link:hover{text-decoration:underline} +.main-column .chat-workspace-section{background:transparent !important;border:none !important;padding:0 !important;box-shadow:none !important;margin-bottom:0} +.tribe-details.chat-workspace-details{background:transparent !important;border:none !important;padding:0 !important;box-shadow:none !important} +.chat-topic{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;text-decoration:none;border:none;background:transparent} +.chat-topic:hover{background:#222} +.chat-topic-active{background:#2f2510} +.chat-topic-title{font-weight:bold;color:#ffa300;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.chat-topic-meta{font-size:0.75rem;color:#888} +.chat-workspace-main{flex:1;min-width:0} +.chat-workspace-empty{display:flex;align-items:center;justify-content:center;border:1px dashed #333;border-radius:8px} +@media (max-width: 900px){.chat-workspace{flex-direction:column}.chat-topics{width:100%;max-height:220px}} +.chat-messages-list{display:flex;flex-direction:column;gap:6px} +.chat-bubble-row{display:flex;justify-content:flex-start;border:none !important;background:transparent !important;padding:0 !important} +.chat-bubble-row-self{justify-content:flex-end} +.chat-message{max-width:85%;padding:7px 12px 5px;border-radius:14px 14px 14px 4px;background:#23242a;word-break:break-word;border:none} +.chat-message-author{background:#1f1800} +.chat-bubble-row-self .chat-message{border-radius:14px 14px 4px 14px;background:#2f2510} +.chat-bubble-sender{display:block;font-size:0.78rem;font-weight:bold;color:#7fb3ff;text-decoration:none;margin-bottom:2px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.chat-bubble-sender:hover{text-decoration:underline} +.chat-bubble-sender-owner{color:#ffa300} +.chat-bubble-time{display:block;font-size:0.68rem;color:#888;text-align:right;margin-top:2px;line-height:1} +.chat-day-separator{display:flex;justify-content:center;margin:10px 0 4px;border:none !important;background:transparent !important;padding:0 !important} +.chat-day-chip{font-size:0.72rem;padding:2px 12px;border-radius:10px;background:#1a1a1a;color:#aaa;border:1px solid #333} +.chat-message.chat-poll{max-width:92%} +.chat-message-text{display:block;font-size:0.9rem;white-space:pre-wrap;word-break:break-word} .calendar-note-card{position:relative;padding:10px 80px 10px 10px;min-height:56px} .calendar-note-delete{position:absolute;top:8px;right:8px;margin:0} .calendar-note-delete button{padding:4px 10px;font-size:0.8rem} @@ -5589,20 +5743,23 @@ a.pm-btn:hover { text-decoration: none } .torrent-table{width:100%;border-collapse:collapse;margin-top:12px} .torrent-table th,.torrent-table td{padding:8px 12px;text-align:left} +.torrent-table td:has(.filter-btn){width:1%;white-space:nowrap} +.torrent-table td .filter-btn{margin:0} .torrent-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} .torrent-download{margin:12px 0} .torrent-download .filter-btn{border:none} .logs-table{width:100%;border-collapse:collapse;margin-top:12px} .logs-table th,.logs-table td{padding:8px 12px;text-align:left;vertical-align:middle;border-bottom:1px solid #222} -.logs-col-actions{vertical-align:middle;text-align:center;white-space:nowrap;background:transparent} -.logs-col-actions form{margin:0;display:inline-block} -.logs-col-actions .filter-btn{min-width:110px} +.logs-col-actions{vertical-align:middle;text-align:right;white-space:nowrap;background:transparent;width:1%} +.logs-row-actions{display:flex;gap:8px;justify-content:flex-end;align-items:center;border:0 !important;background:transparent !important;padding:0 !important} +.logs-col-actions form{margin:0;display:inline-flex} +.logs-col-actions .filter-btn{min-width:0;white-space:nowrap} .logs-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} -.logs-col-date{white-space:nowrap;text-align:center;font-family:monospace;background:transparent} +.logs-col-date{white-space:nowrap;text-align:left;font-family:monospace;background:transparent;width:1%} .logs-date-day{color:#FFA500;font-weight:bold;font-size:0.9rem} .logs-date-time{color:#ddd;font-size:0.85rem} -.logs-col-log{max-width:560px;vertical-align:middle;background:transparent;padding:8px 12px} +.logs-col-log{width:100%;vertical-align:middle;background:transparent;padding:8px 12px;overflow-wrap:anywhere} .logs-col-log>*{background:transparent;border:0;padding:0;margin:0} .logs-entry-label{color:#FFA500;font-weight:bold;margin-bottom:4px !important;background:transparent;border:0} .logs-entry-meta{color:#888;font-size:0.85rem;margin-bottom:6px} @@ -5613,7 +5770,7 @@ a.pm-btn:hover { text-decoration: none } .logs-mode-form{margin:12px 0} .logs-mode-group{display:flex;gap:10px;align-items:center;flex-wrap:wrap} .logs-ai-disabled{color:#e74c3c;font-style:italic;margin:8px 0} -.logs-col-type{white-space:nowrap;text-align:center;font-family:monospace;background:transparent} +.logs-col-type{white-space:nowrap;text-align:left;font-family:monospace;background:transparent;width:1%} .logs-type-text{font-weight:bold;letter-spacing:0.5px;text-transform:uppercase;font-family:monospace;font-size:0.85rem} .logs-type-ai{color:#00b4ff} .logs-type-manual{color:#FFA500} @@ -5704,12 +5861,14 @@ a.pm-btn:hover { text-decoration: none } .larp-my-house{display:flex;align-items:center;gap:8px;margin:8px 0 16px 0;background:transparent !important;border:0 !important;padding:0 !important} .larp-my-house-label{color:#FFD700;font-size:12px;text-transform:uppercase;letter-spacing:1px} .larp-my-house-link{color:#FFA500;font-weight:700} -.larp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;margin-top:12px} +.larp-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-top:12px;align-items:stretch} +.larp-card{display:flex;flex-direction:column} +.larp-card .larp-card-body{flex:1 1 auto;display:flex;flex-direction:column} .larp-card{background:#1A1A1A;border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .larp-card-mine{border:3px solid #FFA500} .larp-card-ruling{border:3px solid #ff6b6b} -.larp-card-image-link{display:block} -.larp-card-image{width:100%;height:160px;object-fit:cover;display:block;background:#0a0a0a} +.larp-card-image-link{display:block;width:100%} +.larp-card-image{width:100%;aspect-ratio:16/10;height:auto;object-fit:cover;object-position:center;display:block;background:#0a0a0a} .larp-card-body{padding:10px 12px;display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important} .larp-card-title-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important} .larp-card-title{color:#FFA500;font-weight:700;font-size:16px;letter-spacing:0.5px} @@ -5727,8 +5886,10 @@ a.pm-btn:hover { text-decoration: none } .larp-detail-body{flex:1 1 320px;display:flex;flex-direction:column;gap:10px;background:transparent !important;border:0 !important;padding:0 !important} .larp-info-table{width:100%;border-collapse:collapse} .larp-info-table td{padding:4px 8px;border-bottom:1px solid #2a2a2a;vertical-align:top} -.larp-detail-description{color:#ccc;line-height:1.5;margin:0} -.larp-actions{margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-wrap:wrap;gap:8px;align-items:center} +.larp-detail-description{color:#FFDD44;line-height:1.6;font-size:15px;margin:0} +.larp-actions{margin-top:12px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-wrap:wrap;gap:8px;align-items:center} +.larp-actions form{margin:0;display:inline-flex;align-items:center} +.larp-actions .filter-btn,.larp-actions button{height:36px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;text-decoration:none} .larp-members-block{margin-top:24px} .larp-members-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .larp-member-row{padding:4px 8px;background:#1A1A1A;border:1px solid #333;border-radius:4px} @@ -5753,9 +5914,6 @@ a.pm-btn:hover { text-decoration: none } .larp-post-badge{display:inline-block;padding:2px 8px;border-radius:10px;background:#3a2300;color:#FFD700;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px} .larp-post-text{color:#ddd;line-height:1.5;margin:0;white-space:pre-wrap;word-break:break-word} .larp-rules{margin-top:14px;display:flex;flex-direction:column;gap:18px;background:transparent !important;border:0 !important;padding:0 !important} -.larp-rules-section{background:#1A1A1A;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} -.larp-rules-section h3{color:#FFA500;margin:0 0 6px 0;font-size:15px} -.larp-rules-section p{color:#ccc;line-height:1.55;margin:6px 0} .larp-rules-list{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .larp-rules-list strong{color:#FFD700} .lan-checkbox-label{display:inline-flex;align-items:center;gap:10px;color:#FFD700;font-size:14px;margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important;cursor:pointer} @@ -5776,9 +5934,9 @@ a.pm-btn:hover { text-decoration: none } .larp-last-attempt-fail{color:#ff8a8a;font-weight:700} .larp-test-form-compact{margin:10px 0 0 0;gap:8px} .larp-test-form-compact .larp-test-question{padding:8px 12px} -.larp-test-form-compact .larp-test-q-text{font-size:13px;margin:0 0 6px 0} -.larp-test-form-compact .larp-test-option{font-size:12px;padding:2px 6px} -.larp-will-test-hint{color:#bbb;font-size:13px;line-height:1.4;margin:4px 0 10px 0} +.larp-test-form-compact .larp-test-q-text{font-size:15px;margin:0 0 8px 0} +.larp-test-form-compact .larp-test-option{font-size:14px;padding:4px 6px} +.larp-will-test-hint{color:#FFA500;opacity:.85;font-size:14px;line-height:1.5;margin:4px 0 12px 0} .larp-academia-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:12px;background:transparent !important;border:0 !important;padding:0 !important} .larp-invite-redeem-form,.larp-invite-create-form,.larp-card-invite-form{display:inline-flex;gap:6px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important;margin:0} .larp-invite-redeem-form input[type=text],.larp-card-invite-form input[type=text]{padding:4px 8px;font-family:monospace;font-size:12px;min-width:280px;width:100%;max-width:360px;box-sizing:border-box} @@ -5789,11 +5947,15 @@ a.pm-btn:hover { text-decoration: none } .larp-invite-banner-code{color:#9aff9a;font-family:monospace;font-size:15px;letter-spacing:1px;margin:0;word-break:break-all} .larp-test-ranking{margin-top:14px;background:transparent !important;border:0 !important;padding:0 !important} .larp-test-ranking-title{color:#FFA500;font-size:15px;margin:0 0 8px 0} -.larp-academia-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;background:transparent !important;border:0 !important;padding:0 !important} -.larp-academia-thumb{display:flex;flex-direction:column;align-items:center;gap:6px;text-decoration:none;background:#1A1A1A;border:1px solid #333;border-radius:8px;padding:10px;transition:border-color 0.2s} +.larp-academia-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;background:transparent !important;border:0 !important;padding:0 !important} +.larp-academia-thumb{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:6px;text-decoration:none;background:#1A1A1A;border:1px solid #333;border-radius:8px;padding:10px;transition:border-color 0.2s} .larp-academia-thumb:hover{border-color:#FFA500} .larp-academia-thumb-mine{border:3px solid #FFA500} -.larp-academia-thumb-image{width:100%;max-width:128px;height:128px;object-fit:cover;border-radius:6px;border:1px solid #FFA500;display:block} +.larp-academia-thumb-image{width:100%;max-width:72px;aspect-ratio:1;height:auto;object-fit:cover;border-radius:6px;border:1px solid #FFA500;display:block;margin:0 auto} +.larp-academia-thumb-table{margin-top:2px} +.larp-academia-thumb-table td{padding:2px 6px;font-size:12px} +.larp-academia-thumb-table .card-label{color:#FFA500;white-space:nowrap} +.larp-academia-thumb-table .card-value{color:#ccc} .larp-academia-thumb-name{color:#FFA500;font-weight:700;font-size:14px;text-transform:uppercase;letter-spacing:0.5px;text-align:center} .larp-test-header{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start;margin:14px 0;background:transparent !important;border:0 !important;padding:0 !important} .larp-test-image{width:180px;height:180px;object-fit:cover;border-radius:8px;border:2px solid #FFA500;display:block;flex:0 0 180px} @@ -5805,10 +5967,10 @@ a.pm-btn:hover { text-decoration: none } .larp-test-cooldown p{color:#FFD700;margin:0 0 8px 0;font-size:14px} .larp-test-form{margin:14px 0;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-direction:column;gap:14px} .larp-test-question{background:#1A1A1A;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} -.larp-test-q-text{color:#ddd;line-height:1.5;margin:0 0 10px 0;font-size:14px} -.larp-test-q-text strong{color:#FFA500} +.larp-test-q-text{color:#FFDD44;line-height:1.5;margin:0 0 10px 0;font-size:15px;font-weight:600} +.larp-test-q-text strong{color:#FFDD44} .larp-test-options{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;align-items:flex-start} -.larp-test-option{color:#ccc;font-size:13px;display:inline-flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;cursor:pointer;padding:4px 6px;border-radius:3px;width:auto;max-width:100%} +.larp-test-option{color:#FFA500;font-size:14px;display:inline-flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;cursor:pointer;padding:4px 6px;border-radius:3px;width:auto;max-width:100%} .larp-test-option:hover{background:#222} .larp-test-option input[type=radio]{accent-color:#FFA500;margin:0;flex:none;width:16px;height:16px} .larp-test-result{background:#1A1A1A;border:1px solid #FFA500;border-radius:8px;padding:18px 22px;margin:14px 0} @@ -5843,7 +6005,11 @@ a.pm-btn:hover { text-decoration: none } .graphos-node-link:hover .graphos-node-label{font-weight:600} .graphos-node-label{font-size:12px;fill:#ddd;text-anchor:middle;pointer-events:none} .graphos-node-label-me{font-weight:600;fill:#ffa500} -.graphos-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;font-size:13px;color:#ddd;margin:0 0 8px} +.graphos-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;justify-content:center;font-size:14px;color:#ddd;margin:14px 0 8px} +.graphos-stats .stats-grid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:18px} +.graphos-stats .stats-kpi{background:#1c1c1c;border:1px solid #444;border-radius:8px;padding:12px 14px;box-shadow:0 1px 3px rgba(0,0,0,0.35)} +.graphos-stats .stats-kpi-label{font-size:14px} +.graphos-stats .stats-kpi-inline .stats-kpi-value{font-size:20px} .graphos-legend-item{display:flex;align-items:center;gap:6px} .graphos-legend-dot{display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid #1a1a1a} .graphos-legend-dot.graphos-node-circle-me{background:#ffa500;border-color:#cc7700} @@ -6032,14 +6198,15 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .search-filters-table td{padding:6px 10px;border:none;vertical-align:middle;background:transparent} .search-filters-table td.card-label{white-space:nowrap;font-weight:600;color:#ffa500} .search-filters-table input[type="datetime-local"], -.search-filters-table input.search-oasis-id{min-width:280px} +.search-filters-table input.search-oasis-id, +.search-filters-table input[type="search"], +.search-filters-table select{min-width:280px;width:100%;box-sizing:border-box} .transfer-category-select{width:auto;min-width:160px;max-width:200px;padding:6px 10px} .spread-row{margin:8px 0;display:flex;justify-content:flex-start} .spread-form{display:inline-block} -.spread-btn{cursor:pointer;font:inherit;padding:4px 10px;border:1px solid #555;border-radius:4px;background:#1a1a1a;color:#ffa500;font-size:14px} -.spread-btn:hover{background:#2a2a2a;border-color:#ffa500} -.spread-btn-on{background:#ffa500;color:#1a1a1a;border-color:#ff7300} -.spread-btn-on:hover{background:#ffb733;color:#000} +.btn-spread-on{background:#ffa300;color:#000} +.btn-spread-on:hover{background:#ffb733;color:#000} +.spread-form .btn-singleview{border-radius:16px;padding:7px 12px} .welcome-header{margin-bottom:18px} .welcome-chat{max-width:760px;margin:0 auto;padding:8px} .welcome-ai-header{display:flex;align-items:center;gap:10px;margin-bottom:10px;color:#ffa500;font-weight:600} @@ -6161,7 +6328,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .chat-card, .opinions-card, .post-card, -.map-card, .mention-card, .trending-card, .bank-claim-card, @@ -6325,7 +6491,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .chat-card .card-field, .opinions-card .card-field, .post-card .card-field, -.map-card .card-field, .mention-card .card-field, .trending-card .card-field, .bank-claim-card .card-field, @@ -6340,6 +6505,8 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} border-bottom: 1px solid #2a2a2a; align-items: baseline; } +.card-field > .card-value:only-child { grid-column: 1 / -1; min-width: 0; } + .card .card-field:last-child, .card-section .card-field:last-child, .card-fields-container .card-field:last-child, @@ -6398,8 +6565,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .card-date-highlight{color:#ffd740;font-size:18px;font-weight:700;margin:6px 0;letter-spacing:.5px;background:transparent !important;border:0 !important;padding:0 !important} .vote-buttons-row-single{display:flex;flex-wrap:wrap;gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .vote-buttons-row-single form{margin:0;display:inline-block} -.card-visit-btn-centered{display:flex;justify-content:center;margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important} -.card-visit-btn-centered form{margin:0} .tribe-card-body .voting-buttons{padding:0;background:transparent !important;border:0 !important;margin:8px 0 0 0} .card-assigned-list{display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0} .card-assigned-list a{display:block} @@ -6418,6 +6583,8 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .activity-card-header{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px;border:none;background:transparent;padding:0} .activity-card-header .card-chips-row{margin:0} .content-actions{display:flex;justify-content:right;align-items:center;gap:12px;flex-wrap:wrap;border:none;background:transparent;padding:0} +.tribe-side .content-actions{flex-wrap:nowrap;gap:6px} +.tribe-side .content-actions .btn-singleview{padding:6px 9px;font-size:1em} .trending-card.own-content{box-shadow:inset 3px 0 0 #ffa300} .trending-dominant-sep{margin:0 8px;opacity:0.5} .trending-dominant-text,.trending-total-count{font-weight:700} @@ -6477,7 +6644,7 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .industry-hero-image,.tribe-card-hero-image{max-width:100%;height:auto;border-radius:6px} .industry-search{margin:8px 0} .industry-card-noimage{display:flex;align-items:center;justify-content:center;min-height:120px;font-size:48px;opacity:.5} -.industry-bot-notification .pm-title{margin-top:4px} +.industry-bot-notification .pm-title,.housing-bot-notification .pm-title{margin-top:4px} .industry-table{width:100%;border-collapse:collapse;margin:8px 0} .industry-table th,.industry-table td{text-align:left;padding:6px 8px;border-bottom:1px solid rgba(128,128,128,.3)} .industry-contrib-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0} @@ -6499,7 +6666,7 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .industry-actions .project-control-select,.industry-actions .project-control-btn,.industry-actions select,.industry-actions button{margin:0} .industry-actions button,.industry-actions .project-control-select{display:inline-flex;align-items:center;justify-content:center;height:38px;box-sizing:border-box} .card-chips-row .spread-form{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center} -.card-chips-row .spread-form .spread-btn{margin:0} +.card-chips-row .spread-form .btn-singleview{margin:0} .industry-build-notes{margin:22px 0} .industry-dates-row{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0;margin:8px 0} .industry-dates-row p{margin:0} @@ -6549,10 +6716,17 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .dev-code-row td{border:none;vertical-align:top} .dev-code-num{text-align:right;padding:0 14px 0 4px;width:1%;min-width:52px;white-space:nowrap;opacity:.5;user-select:none;border-right:1px solid rgba(128,128,128,.25) !important} .dev-code-line{padding-left:16px} -.welcome-banner .update-banner-icon{animation:none} +.ai-suggestion-banner .update-banner-link{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60vw} .welcome-banner .welcome-banner-close{margin:0;padding:0;border:none;background:transparent;box-shadow:none;display:inline-flex;align-items:center} .welcome-banner-close-btn{background:transparent;border:none;color:inherit;cursor:pointer;font-size:1em;line-height:1;padding:0 4px;margin:0;opacity:.6} .welcome-banner-close-btn:hover{opacity:1} +.welcome-ux-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;border:none !important;background:transparent !important} +.welcome-ux-option{display:flex;flex-direction:column;gap:6px;align-items:center;border:1px solid rgba(128,128,128,.35);border-radius:8px;padding:10px;cursor:pointer;background:transparent} +.welcome-ux-option:has(input:checked){border-color:#ffa300;background:rgba(255,163,0,.07)} +.welcome-ux-option input{margin:0} +.welcome-ux-shot{width:100%;border-radius:6px;border:1px solid rgba(128,128,128,.25)} +.welcome-ux-label{font-weight:bold} +@media (max-width: 700px){.welcome-ux-grid{grid-template-columns:1fr}} .welcome-progress,.welcome-steps,.welcome-complete,.welcome-footer,.welcome-action,.welcome-step-head{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important} .welcome-progress{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin:12px 0} .welcome-progress p{margin:0} @@ -6584,3 +6758,846 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .welcome-step-warning{margin:0 0 12px 0} .welcome-oasisid{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 14px 0;word-break:break-all} .welcome-oasisid .user-link{user-select:all} +.housing-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:20px} +.housing-grid .tribe-card{margin-bottom:0} +.housing-card .card-field{border-bottom:none} +.housing-info-table{table-layout:auto} +.housing-info-table .tribe-info-label{width:1%;white-space:nowrap} +.housing-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere} +.housing-stars{color:#FFA500;font-size:14px;letter-spacing:1px} +.media-gallery{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 12px 0} +.media-gallery .gallery{padding:0} +.housing-search .filter-box{margin:0} +.housing-place-input{height:36px;min-width:170px;width:auto;flex:1 1 170px} +.media-form-gallery{padding:0;margin:10px 0} +.media-form-gallery .gallery-item{cursor:default} +.housing-keep-photos{display:inline-flex;align-items:center;gap:6px;margin:0 0 10px 0} +.housing-keep-photos input[type="checkbox"]{width:auto;margin:0} +.tribe-main .housing-opinions{border:none;background:transparent} +.housing-form input[type="file"]{width:auto} +.vote-form-error{margin:0 0 14px 0} +.vote-form-error .error-title{margin:0 0 4px 0;font-weight:bold} +.vote-form-error p{margin:0} +.media-file-row{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 6px 0} +.media-file-row input[type="file"]{width:auto;margin:0} +.media-add-btn{margin:0} +.media-video{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 10px 0} +.media-video-player{max-width:100%;border-radius:6px;display:block} +.media-form-video{max-width:320px} +.media-video-centered{display:flex;justify-content:center;margin-top:12px} +.media-video-centered .media-video-player{max-width:720px;width:100%} +.housing-card-video{display:flex;justify-content:center} +.housing-card-hero-video{width:100%;max-height:220px;border-radius:6px;display:block} +.housing-status-row,.housing-visibility-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px} +.housing-status-row form,.housing-visibility-row form{flex-basis:100%;margin:4px 0 0 0} +.housing-stars{display:block;margin:2px 0 6px 0} +.media-form-gallery .gallery-item{display:flex;flex-direction:column;align-items:center;gap:6px} +.media-remove-btn{height:28px;padding:0 10px;border:1px solid rgba(128,128,128,.5);border-radius:4px;background:transparent;color:inherit;cursor:pointer;font-size:.8em} +.media-form-video{display:flex;flex-direction:column;align-items:flex-start;gap:8px} +.spread-edit-warning{margin:0 0 14px 0} +.spread-edit-warning p{margin:0} +.market-opinions{border:none !important;background:transparent !important;padding:0 !important;margin:12px 0} +.housing-status-row .project-control-form{flex-basis:100%;margin:4px 0 0 0} +.task-assign-row{margin:8px 0} +.task-assign-row form{margin:0} +.reports-subfilters{margin-top:8px;align-items:center} +.reports-subfilters .card-label{margin:0 4px 0 8px;white-space:nowrap} +.stats-kpi-inline{display:flex;flex-direction:row;align-items:baseline;gap:6px;flex-wrap:wrap} +.stats-kpi-inline .stats-kpi-value{font-size:16px} +.stats-kpi-inline .stats-kpi-label{white-space:nowrap} +.content-actions .content-action-form{margin:0;display:inline-flex} +.content-actions .btn-pin-on{background:#ffa300;color:#000} +.content-actions .btn-pin-on:hover{background:#ffb733;color:#000} +.content-actions .spread-form{margin:0;display:inline-flex} + +.blog-card-body, +.blog-detail-body, +.mention-card-body { + border: none !important; + background: transparent !important; +} + +.blog-card-text, +.blog-detail-text, +.mention-text { + line-height: 1.6; + word-wrap: break-word; + overflow-wrap: anywhere; + border: none !important; + background: transparent !important; +} + +.blog-card-text img, +.blog-detail-text img { + max-width: 100%; + height: auto; + display: block; + margin: 10px 0; +} + +.blog-card-text pre, +.blog-detail-text pre { + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +} + +.blog-card-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 6px 0; + border: none !important; + background: transparent !important; +} + +.blog-allow-comments { + display: inline-flex; + align-items: center; + gap: 6px; + border: none !important; + background: transparent !important; +} + +.blog-allow-comments input[type="checkbox"] { + width: auto; + margin: 0; +} + +.logs-public-list { + display: flex; + flex-direction: column; + gap: 12px; + border: none !important; + background: transparent !important; +} + +.logs-public-body { + border: none !important; + background: transparent !important; +} + +.logs-public-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 6px 0; + border: none !important; + background: transparent !important; +} + +.logs-published-hint { + margin-top: 10px; + opacity: .8; + font-size: .9em; +} + +.poll-card-body, +.poll-detail-body, +.poll-card-meta, +.poll-switch, +.poll-ballot, +.poll-option-row { + border: none !important; + background: transparent !important; +} + +.poll-results { + width: 100%; + border-collapse: collapse; + margin: 8px 0; +} + +.poll-results td { + padding: 4px 6px; + vertical-align: middle; +} + +.poll-result-option { + white-space: normal; + overflow-wrap: anywhere; +} + +.poll-result-count { + text-align: right; + white-space: nowrap; +} + +.poll-bar { + width: 100%; + min-width: 60px; + height: 10px; + border: 1px solid rgba(128, 128, 128, .5); + border-radius: 5px; + overflow: hidden; + background: transparent !important; +} + +.poll-bar-fill { + height: 100%; + background: rgba(128, 128, 128, .55) !important; + border: none !important; +} + +.poll-bar-fill-0 { width: 0%; } +.poll-bar-fill-5 { width: 5%; } +.poll-bar-fill-10 { width: 10%; } +.poll-bar-fill-15 { width: 15%; } +.poll-bar-fill-20 { width: 20%; } +.poll-bar-fill-25 { width: 25%; } +.poll-bar-fill-30 { width: 30%; } +.poll-bar-fill-35 { width: 35%; } +.poll-bar-fill-40 { width: 40%; } +.poll-bar-fill-45 { width: 45%; } +.poll-bar-fill-50 { width: 50%; } +.poll-bar-fill-55 { width: 55%; } +.poll-bar-fill-60 { width: 60%; } +.poll-bar-fill-65 { width: 65%; } +.poll-bar-fill-70 { width: 70%; } +.poll-bar-fill-75 { width: 75%; } +.poll-bar-fill-80 { width: 80%; } +.poll-bar-fill-85 { width: 85%; } +.poll-bar-fill-90 { width: 90%; } +.poll-bar-fill-95 { width: 95%; } +.poll-bar-fill-100 { width: 100%; } + +.poll-option-row { + margin: 4px 0; +} + +.poll-option-row input[type="radio"], +.poll-option-row input[type="checkbox"] { + width: auto; + margin-right: 6px; +} + +.poll-switch { + display: flex; + align-items: center; + margin: 6px 0; + padding: 0 !important; + border: none !important; + background: transparent !important; +} + +.poll-switch input[type="checkbox"] { + width: auto; + margin: 0 6px 0 0; +} + +.chat-poll-form form { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.chat-poll-form input[type="text"], +.chat-poll-form textarea { + width: 100%; + max-width: 640px; + margin: 0; +} + +.chat-poll { + display: block; +} + +.chat-poll-head { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 6px; +} + +.chat-poll-tag { + font-size: .75em; + text-transform: uppercase; + border: 1px solid rgba(128, 128, 128, .5); + border-radius: 4px; + padding: 1px 6px; +} + +.chat-poll-question { + font-weight: bold; + margin: 4px 0 8px 0; +} + +.chat-poll-meta { + margin: 6px 0; +} + +.chat-poll-form { + border: 1px solid rgba(128, 128, 128, .4); + border-radius: 6px; + padding: 8px 12px; + margin-bottom: 12px; + background: transparent !important; +} + +.chat-poll-summary { + display: inline-block !important; + width: auto !important; + max-width: max-content; + box-sizing: border-box; + cursor: pointer; + font: inherit; + font-weight: bold; + padding: 10px 20px; + margin-bottom: 10px; + border: 1px solid rgba(128, 128, 128, .5) !important; + border-radius: 5px; + background: rgba(128, 128, 128, .12) !important; + transition: background-color .2s, border-color .2s; + list-style: none; +} + +.chat-poll-summary:hover { background: rgba(128, 128, 128, .25) !important; } +.chat-poll-summary::-webkit-details-marker { display: none; } +.chat-poll-summary::after { content: " \25BE"; opacity: 0.85; } +details[open] > .chat-poll-summary::after { content: " \25B4"; } + +.poll-form-fields { + display: flex; + flex-direction: column; + gap: 4px; +} + +.larp-poll { + display: block; +} + +.reply-context { + border-left: 3px solid rgba(128, 128, 128, .6); + padding-left: 10px; + margin-bottom: 8px; + opacity: .85; +} + +.reply-context-meta { + font-size: .85em; +} + +.reply-context-text { + font-size: .85em; + max-height: 60px; + overflow: hidden; + margin-top: 4px; + font-weight: bold; +} + +.forum-reply-body { + margin-bottom: 12px; +} + +.forum-reply-text { + margin: 0 0 8px 0; + overflow-wrap: anywhere; +} + +.chat-thread-details, +.chat-thread-meta { + border: none !important; + background: transparent !important; +} + +.chat-thread-summary { + cursor: pointer; + font-weight: bold; + margin: 6px 0 6px 16px; +} + +.thread-reply-text { + white-space: pre-wrap; +} + +.event-recurrence-block, +.event-occurrences { + border: none !important; + background: transparent !important; + margin: 8px 0; +} + +.event-occurrence-list { + padding-left: 22px; + margin: 6px 0; +} + +.cv-ai-box, +.cv-match-row, +.inhabitant-subfilters { + border: none !important; + background: transparent !important; +} + +.cv-ai-box { + border: 1px solid rgba(128, 128, 128, .4) !important; + border-radius: 6px; + padding: 10px 14px; + margin-bottom: 12px; +} + +.cv-ai-box input[type="number"] { + width: auto; + min-width: 90px; +} + +.inhabitant-subfilters { + margin-top: 4px; +} + +.pads-grid { + grid-template-columns: repeat(auto-fill, minmax(min(100%, 520px), 1fr)); +} + +.poll-hue-0 { background: rgba(76,175,80,.75) !important; } +.poll-hue-1 { background: rgba(33,150,243,.75) !important; } +.poll-hue-2 { background: rgba(255,152,0,.75) !important; } +.poll-hue-3 { background: rgba(156,39,176,.75) !important; } +.poll-hue-4 { background: rgba(0,188,212,.75) !important; } +.poll-hue-5 { background: rgba(233,30,99,.75) !important; } +.poll-hue-6 { background: rgba(139,195,74,.75) !important; } +.poll-hue-7 { background: rgba(121,85,72,.75) !important; } + +.poll-result-mine .poll-result-option { + font-weight: bold; +} + +.poll-deadline { + margin: 6px 0; + border: none !important; + background: transparent !important; +} + +.chat-poll-actions { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 10px; + border: none !important; + background: transparent !important; +} + +.report-category-select { + width: auto; + max-width: 260px; +} + +.data-search { + margin-top: 10px; +} + +.data-cohesion { + display: flex; + flex-direction: column; + gap: 10px; + border: none !important; + background: transparent !important; +} + +.data-cohesion-value { + font-size: 2em; + font-weight: bold; + margin: 0; +} + +.data-cohesion-hint { + opacity: .75; + margin: 0; +} + +.data-cohesion-figures { + display: flex; + flex-wrap: wrap; + gap: 24px; + border: none !important; + background: transparent !important; +} + +.data-figure { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 6px 14px; + border-radius: 6px; +} + +.data-figure-value { + font-size: 1.4em; + font-weight: bold; +} + +.data-figure-label { + font-size: .85em; + opacity: .7; + text-transform: uppercase; +} + +.bookmark-subtitle{color:#FFDD44;margin:2px 0 6px 0;font-size:15px} +.data-common-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0;background:transparent !important;border:0 !important;padding:0 !important} +.data-common-row .card-label{margin:0} +.data-common-row .tag-link{margin:0;padding:0.15em 0.55em;line-height:1.4} +.data-you-chip{margin-left:8px} +.data-top-terms { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + border: none !important; + background: transparent !important; +} + +.data-top-terms .tag-link { + font-size: 1.15em; +} + +.data-scale-0 { background: rgba(233, 30, 99, .75) !important; } +.data-scale-1 { background: rgba(255, 152, 0, .75) !important; } +.data-scale-2 { background: rgba(255, 214, 0, .75) !important; } +.data-scale-3 { background: rgba(139, 195, 74, .75) !important; } +.data-scale-4 { background: rgba(76, 175, 80, .75) !important; } + +.data-list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + border: none !important; + background: transparent !important; +} + +@media (max-width: 900px) { + .data-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 600px) { + .data-list { grid-template-columns: 1fr; } +} + + +.data-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 14px; + border-radius: 8px; + position: relative; +} + +.data-score-value, +.data-links-count { + font-weight: bold; + white-space: nowrap; +} + +.data-side-top { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + padding-right: 40px; + border: none !important; + background: transparent !important; +} + +.data-side-top .data-score-label { + margin-left: auto; + margin-bottom: 0; + white-space: nowrap; +} + +.data-side-top .data-kind-chip { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.data-card .data-side-visit { + position: absolute; + top: 10px; + right: 12px; + margin: 0; +} + +.data-card .data-side-visit { + background: transparent !important; + border: 1px solid rgba(128, 128, 128, .45) !important; + border-radius: 10px; + padding: 2px 9px !important; + font-size: .9em; + line-height: 1.4; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; +} + +.data-kind-chip { + display: inline-block; + font-size: .68em; + letter-spacing: .04em; + padding: 2px 7px; + border: 1px solid rgba(128, 128, 128, .45); + border-radius: 10px; + opacity: .85; + white-space: nowrap; +} + +.data-card-title { + font-weight: bold; + font-size: .95em; + line-height: 1.25; + min-width: 0; + overflow-wrap: anywhere; +} + +.data-best-card { + background: #26200f !important; + border: 1px solid rgba(255, 163, 0, .35) !important; +} + +.data-best-score { + font-size: 1.15em; +} + + +.data-card-author { + font-size: .78em; + opacity: .7; + max-width: 100%; + padding-left: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.data-cohesion-label { + font-size: .5em; + vertical-align: middle; + opacity: .75; +} + +.card-engage { + display: grid; + grid-template-columns: 1fr 1fr; + align-items: start; + gap: 8px; + margin: 8px 0; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.card-engage > * { + min-width: 0; +} + +.card-engage > details[open] { + grid-column: 1 / -1; +} + +.card-engage > details[open] ~ details { + grid-column: 1 / -1; +} + +.card-engage .opinions-voting-collapse, +.card-engage .comments-collapse { + margin: 0; +} + +.card-engage .opinions-summary, +.card-engage .comments-summary { + width: 100% !important; + max-width: none; + text-align: center; +} + +@media (max-width: 600px) { + .card-engage { grid-template-columns: 1fr; } +} + +.activity-search input[type="text"], +.blockexplorer-search-pair input[type="text"], +.blockexplorer-search-dates input[type="text"], +.blockexplorer-search-actions input[type="text"], +.logs-toolbar input[type="text"], +.search-filters-row input[type="text"], +.tribe-search-form input[type="text"], +.filter-box input[type="text"], +.ui-toolbar input[type="text"] { + margin: 0; +} + +.calendar-invite-actions, +.calendar-owner-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; +} + +.calendar-open-invite-block { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.image-container { + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.activity-sub-filter { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 4px 0 14px 0; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.activity-sub-filter .sub-filter-form { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0; +} + +.larp-academia-hint .larp-my-house-label { + opacity: .85; +} + +.larp-academia-hint .larp-my-house-link { + font-weight: bold; +} + +.oasis-footer-version { + font-weight: bold; +} + +.larp-test-panel { + border: 1px solid rgba(128, 128, 128, .4) !important; + border-radius: 6px; + padding: 12px 16px; + margin-top: 16px; + background: transparent !important; +} + +.larp-test-panel-hint { + color: #FFA500; + opacity: .85; + font-size: 14px; + line-height: 1.5; + margin: 4px 0 12px 0; +} + +@media (max-width: 900px) { + .larp-academia-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 560px) { + .larp-academia-grid { grid-template-columns: 1fr; } +} + +@media (max-width: 900px) { + .larp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 560px) { + .larp-grid { grid-template-columns: 1fr; } +} + +.chat-jump-row { + display: flex; + justify-content: flex-end; + margin-bottom: 8px; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.chat-latest-anchor { + scroll-margin-top: 16px; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.ai-prompt-form{margin-bottom:0} +.ai-submit-row{display:flex;gap:1.5em;justify-content:flex-end;align-items:center;margin-top:.7em} +.ai-clear-row{display:flex;justify-content:flex-end;margin-bottom:2em} +.ai-clear-form{display:inline} +.ai-clear-btn{background:#b80c09;color:#fff;border:none;padding:.4em 1.2em;border-radius:6px;cursor:pointer;font-size:1em;margin-left:1em} +.user-prompt{margin-bottom:2em;font-size:.95em;color:#888} +.user-prompt-text{font-style:italic} +.chat-entry{margin-bottom:2em;position:relative;background:#191919;border-radius:10px;box-shadow:0 0 8px #0004;padding-top:1.8em} +.chat-entry-timestamp{position:absolute;top:.5em;right:1.3em;font-size:.92em;color:#888} +.user-question{margin-bottom:.75em} +.ai-response{max-width:800px;margin:auto;background:#111;padding:1.25em;border-radius:6px;font-family:sans-serif;line-height:1.6;color:#ffcc00} +.ai-response p{margin-bottom:1.2em} +.ai-train-bar{display:flex;align-items:center;gap:12px;margin:12px auto 8px auto;max-width:800px;padding:8px 0;border-top:1px solid #2a2a2a;flex-wrap:wrap} +.ai-snippets-used{color:#9aa;font-size:.95em} +.ai-train-approved{color:#5ad25a;font-weight:600} +.ai-train-rejected{color:#ff6b6b;font-weight:600} + +.modules-counts{margin-bottom:16px} +.modules-count-value{color:#888} +.modules-count-list{list-style-type:none;padding:0;margin:0} +.modules-count-item{font-size:18px;color:#555;margin:8px 0} +.modules-preset-form{display:inline;margin:0} +.module-status-cell{text-align:center} +.save-button-container{margin-top:20px;text-align:center} + +.votes-count-negative{margin-left:12px} +.forum-thread-header{display:flex;align-items:flex-start} +.root-vote-col{width:60px;text-align:center} +.forum-thread-header .forum-main-col{flex:1;padding-left:10px} +.new-message-wrapper{margin-top:12px} +.new-message-textarea{width:100%} +.new-message-form .forum-send-btn{margin-top:4px} + +.card-field-row{display:flex;gap:.6em;flex-wrap:wrap} +.card-section-action{margin-top:6px} +.card-section-text{margin:.4rem 0} +.post-text-clamped{max-height:180px;overflow:hidden} +.reply-context{border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:.85} +.reply-context-label{font-size:.85em} +.reply-context-text{font-size:.85em;max-height:80px;overflow:hidden;margin-top:4px} +.activity-spread-clamped{max-height:200px;overflow:hidden} +.activity-spread-missing{opacity:.6;font-style:italic} + +.search-select{position:relative;z-index:10} +.card-field-stacked{display:flex;flex-direction:column} +.card-value-pre{white-space:pre-wrap;margin-top:4px} + +.trending-container{display:grid;grid-template-columns:1fr;gap:20px} +.feed-vote-sep{margin:0 8px;opacity:.5} +.feed-vote-dominant{font-weight:700} +.image-container-row,.video-container-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} +.video-container-row{flex-direction:column;align-items:stretch;width:100%} +.video-container-row video{width:100%;max-height:640px} +.vote-no-quorum{color:#ffcc00;font-weight:bold} + +.pixelia-swatch-000000{background-color:#000000} +.pixelia-swatch-ffffff{background-color:#ffffff} +.pixelia-swatch-17f018{background-color:#17f018} +.pixelia-swatch-ffbb00{background-color:#ffbb00} +.pixelia-swatch-ff0000{background-color:#ff0000} +.pixelia-swatch-0000ff{background-color:#0000ff} +.pixelia-swatch-ffff00{background-color:#ffff00} +.pixelia-swatch-00ff00{background-color:#00ff00} +.pixelia-swatch-00ffff{background-color:#00ffff} +.pixelia-swatch-ff00ff{background-color:#ff00ff} +.pixelia-swatch-a52a2a{background-color:#a52a2a} +.pixelia-swatch-800080{background-color:#800080} +.pixelia-swatch-808000{background-color:#808000} +.pixelia-swatch-00bfff{background-color:#00bfff} +.pixelia-swatch-d3d3d3{background-color:#d3d3d3} +.pixelia-swatch-ff6347{background-color:#ff6347} diff --git a/src/views/activity_view.js b/src/views/activity_view.js index 8440cd7..f922d5c 100644 --- a/src/views/activity_view.js +++ b/src/views/activity_view.js @@ -1,8 +1,8 @@ -const { div, h2, p, section, button, form, a, input, label, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions, renderHiveNav } = require('./main_views'); +const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views'); const opinionCategories = require('../backend/opinion_categories'); -const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','transfer']); +const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent']); const OPINION_ROUTES = { feed: (id) => `/feed/opinions/${encodeURIComponent(id)}`, bookmark: (id) => `/bookmarks/opinions/${encodeURIComponent(id)}`, @@ -11,11 +11,11 @@ const OPINION_ROUTES = { torrent: (id) => `/torrents/opinions/${encodeURIComponent(id)}`, video: (id) => `/videos/opinions/${encodeURIComponent(id)}`, document: (id) => `/documents/opinions/${encodeURIComponent(id)}`, - votes: (id) => `/votes/opinions/${encodeURIComponent(id)}`, - transfer: (id) => `/transfers/opinions/${encodeURIComponent(id)}` + votes: (id) => `/votes/opinions/${encodeURIComponent(id)}` }; const moment = require("../server/node_modules/moment"); const { renderUrl } = require('../backend/renderUrl'); +const { letterOf } = require('./polls_view'); const { getConfig } = require("../configs/config-manager.js"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); @@ -228,7 +228,14 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { } exports.renderActionCards = renderActionCards; -function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { +const SPREADABLE_TYPES = new Set([ + 'post', 'audio', 'video', 'image', 'document', 'torrent', 'bookmark', + 'event', 'calendar', 'task', 'votes', 'vote', 'market', 'shop', 'shopProduct', + 'project', 'transfer', 'housing', 'job', 'report', 'industry', 'industryBuild', 'industryBlueprint', + 'chat', 'chatMessage', 'pad', 'padEntry', 'forum', 'map', 'poll', 'blog' +]); + +function renderActionCards(actions, userId, allActions, spreadMap = new Map(), extras = {}) { const all = Array.isArray(allActions) ? allActions : actions; const byIdAll = new Map(); for (const a0 of all) { @@ -343,7 +350,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } else if (type === 'shopProduct') { headerText = `[SHOP · PRODUCT]`; } else if (type === 'chat') { - headerText = `[CHAT \u00b7 NEW]`; + headerText = `[CHAT \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}]`; } else if (type === 'pad') { headerText = `[PAD · ${String(i18n.padNew || 'NEW').toUpperCase()}]`; } else if (type === 'ubiClaim') { @@ -551,29 +558,19 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'tribe') { - const { title, image, description, location, tags, inviteMode, isAnonymous } = content; + const { title, image, description, isAnonymous } = content; if (isAnonymous === true) { skip = true; } - const validTags = Array.isArray(tags) ? tags : []; cardBody.push( div({ class: 'card-section tribe' }, h2({ class: 'tribe-title' }, a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title) ), - div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, - location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "", - typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "", - inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "" - ), image ? (/^(\/|https?:)/.test(String(image)) ? img({ src: image, class: 'feed-image tribe-image' }) : renderMediaBlob(image, null)) : null, - p({ class: 'tribe-description' }, ...renderUrl(description || '')), - validTags.length - ? div({ class: 'card-tags' }, validTags.map(tag => - a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) - : "" + p({ class: 'tribe-description' }, ...renderUrl(description || '')) ) ); } @@ -745,7 +742,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'event') { - const { title, description, date, location, price, attendees, organizer, isPublic } = content; + const { title, description, date, location, price, attendees, isPublic } = content; cardBody.push( div({ class: 'card-section event' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)), @@ -754,7 +751,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "", price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "", br(), - organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), userLink(organizer)) : "", Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : "" ) ); @@ -838,9 +834,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { (url) => `${url}` ); - bodyNode = div({ class: 'post-text', style: 'max-height:180px;overflow:hidden;', innerHTML: sanitizeHtml(linkified) }); + bodyNode = div({ class: 'post-text post-text-clamped', innerHTML: sanitizeHtml(linkified) }); } else { - bodyNode = p({ class: 'post-text post-text-pre', style: 'max-height:180px;overflow:hidden;' }, ...renderUrlPreserveNewlines(displayText)); + bodyNode = p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderUrlPreserveNewlines(displayText)); } const threadId = getThreadIdFromPost(action); const replyToId = getReplyToIdFromPost(action, byIdAll); @@ -855,18 +851,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section post' }, isReply ? div( - { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, - span({ style: 'font-size:0.85em;' }, + { class: 'reply-context' }, + span({ class: 'reply-context-label' }, a({ href: ctxHref, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' ), - parentText ? p({ class: 'post-text reply-context-text post-text-pre', style: 'font-size:0.85em;max-height:80px;overflow:hidden;margin-top:4px;' }, ...renderUrlPreserveNewlines(parentText)) : '' + parentText ? p({ class: 'post-text reply-context-text post-text-pre' }, ...renderUrlPreserveNewlines(parentText)) : '' ) : '', contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '', bodyNode, isTruncated && threadId - ? div({ style: 'margin-top:6px;' }, + ? div({ class: 'card-section-action' }, a({ href: `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(action.id || threadId)}`, class: 'filter-btn' }, i18n.keepReading || 'Keep reading...') ) : '' @@ -893,12 +889,16 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { span({ class: 'pm-exposition-text' }, `${String(i18n.typePost || 'POST').toUpperCase()} · THREAD`) ) ), - renderContentActions(threadId, href) + renderContentActions(threadId, href, { + author: action.author, + spread: spreadMap.get(threadId) || null, + ...favOptsFor('post', threadId, extras) + }) ), div({ class: 'card-body' }, root && root.text ? div({ class: 'card-section' }, - p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(root.text)) + p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(root.text)) ) : '', div({ class: 'card-section' }, @@ -906,7 +906,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; return div({ class: 'thread-reply-item' }, div({ class: 'thread-reply' }, - r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : '' + r.text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(r.text)) : '' ), div({ class: 'card-footer thread-reply-footer' }, span({ class: 'date-link' }, rDate), @@ -937,30 +937,46 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { 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' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} · THREAD`) + span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}`) ) ), - renderContentActions(chatRoot, href) + renderContentActions(chatRoot, href, { + author: action.author, + reportTitle: chatTitle, + spread: spreadMap.get(chatRoot) || null, + ...favOptsFor('chat', chatRoot, extras) + }) ), div({ class: 'card-body' }, div({ class: 'card-section chat' }, - div({ class: 'card-field' }, span({ class: 'card-value' }, a({ href, class: 'user-link' }, chatTitle))), - chatDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, chatDesc)) : '' + div({ class: 'card-field' }, a({ href, class: 'card-value user-link' }, chatTitle)), + chatDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, chatDesc)) : '', + div({ class: 'card-field chat-thread-meta' }, + span({ class: 'card-label' }, `${i18n.chatParticipants}: `), + span({ class: 'card-value' }, String(c.members || 0)), + span({ class: 'card-label' }, ` · ${i18n.chatMessagesLabel}: `), + span({ class: 'card-value' }, String(c.messageCount || show.length)) + ) ), - div({ class: 'card-section' }, - show.map(r => { - const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; - return div({ class: 'thread-reply-item' }, - div({ class: 'thread-reply' }, - r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : '' - ), - div({ class: 'card-footer thread-reply-footer' }, - span({ class: 'date-link' }, rDate), - userLink(r.author, action.authorNames && action.authorNames[r.author]) - ) - ); - }) - ) + show.length + ? details({ class: 'chat-thread-details' }, + summary({ class: 'comments-summary chat-thread-summary' }, i18n.keepReading || 'Keep reading...'), + div({ class: 'card-section' }, + show.map(r => { + const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; + return div({ class: 'thread-reply-item' }, + div({ class: 'thread-reply' }, + r.text ? p({ class: 'post-text thread-reply-text' }, ...renderUrlPreserveNewlines(r.text)) : '' + ), + div({ class: 'card-footer thread-reply-footer' }, + span({ class: 'date-link' }, rDate), + userLink(r.author, action.authorNames && action.authorNames[r.author]) + ) + ); + }) + ) + ) + : null ), p({ class: 'card-footer' }, span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `), @@ -976,9 +992,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const linkText = (title && String(title).trim()) ? title : ''; cardBody.push( div({ class: 'card-section forum' }, - div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" }, - span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '), - a({ href: `/forum/${encodeURIComponent(linkKey)}`, style: "font-weight:800;color:#4fc3f7;" }, linkText) + div({ class: 'card-field' }, + linkKey + ? a({ href: `/forum/${encodeURIComponent(linkKey)}`, class: 'card-value user-link' }, linkText || linkKey) + : span({ class: 'card-value' }, linkText) ) ) ); @@ -993,15 +1010,15 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section forum' }, div( - { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, - span({ style: 'font-size:0.85em;' }, + { class: 'reply-context' }, + span({ class: 'reply-context-meta' }, a({ href: `/forum/${encodeURIComponent(hrefKey)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' ), - parentTitle ? p({ class: 'post-text reply-context-text', style: 'font-size:0.85em;max-height:60px;overflow:hidden;margin-top:4px;font-weight:bold;color:#4fc3f7;' }, parentTitle) : '' + parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : '' ), - div({ class: 'card-field', style: 'margin-bottom:12px;' }, - p({ style: "margin:0 0 8px 0; word-break:break-all;" }, ...renderUrl(text)) + div({ class: 'card-field forum-reply-body' }, + p({ class: 'forum-reply-text' }, ...renderUrl(text)) ) ) ); @@ -1044,8 +1061,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '', spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '', spreadExcerpt - ? div({ class: 'post-text activity-spread-text post-text-pre', style: 'max-height:200px;overflow:hidden;' }, ...renderUrlPreserveNewlines(spreadExcerpt)) - : div({ class: 'post-text activity-spread-text', style: 'opacity:0.6;font-style:italic;' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), + ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderUrlPreserveNewlines(spreadExcerpt)) + : div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), spreadOriginalAuthor ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.spreadBy || 'By') + ': '), @@ -1059,7 +1076,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ) : '', link - ? div({ style: 'margin-top:6px;' }, + ? div({ class: 'card-section-action' }, a({ href: `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}`, class: 'filter-btn' }, i18n.viewDetails || 'View details') ) : '' @@ -1205,7 +1222,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { h2({ class: 'tribe-title' }, a({ href: `/industry/${encodeURIComponent(facKey)}`, class: "user-link" }, name || (i18n.industryTitle || 'Industry')) ), - div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, + div({ class: 'card-field-row' }, sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industrySector || 'Sector').toUpperCase() + ':'), span({ class: 'card-value' }, String(sector).toUpperCase())) : "", status ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, statusValue)) : "", membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryMembershipPolicy || 'Membership policy').toUpperCase() + ':'), span({ class: 'card-value' }, policyValue)) : "" @@ -1233,7 +1250,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { h2({ class: 'tribe-title' }, a({ href: `/industry/build/${encodeURIComponent(buildKey)}`, class: "user-link" }, title || (i18n.industryBuild || 'Build')) ), - (buildStatus || startDate || endDate) ? div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, + (buildStatus || startDate || endDate) ? div({ class: 'card-field-row' }, buildStatus ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryBuildStatus_' + buildStatus] || buildStatus).toUpperCase())) : "", startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildStart || 'Start date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(startDate).format('YYYY-MM-DD'))) : "", endDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildEnd || 'End date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(endDate).format('YYYY-MM-DD'))) : "", @@ -1258,7 +1275,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ? a({ href: `/industry/${encodeURIComponent(facility)}`, class: "user-link" }, name || (i18n.industryBlueprint || 'Blueprint')) : (name || (i18n.industryBlueprint || 'Blueprint')) ), - div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, + div({ class: 'card-field-row' }, outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryOutputKind || 'Product type').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + outKind] || outKind).toUpperCase())) : "", laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryLaborHours || 'Labor hours').toUpperCase() + ':'), span({ class: 'card-value' }, String(laborHours))) : "", estTotal != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryEstTotal || 'Estimated price').toUpperCase() + ':'), span({ class: 'card-value' }, `${Number(estTotal).toFixed(2)} ECO`)) : "" @@ -1290,10 +1307,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const chatImageNode = renderMediaBlob(image); cardBody.push( div({ class: 'card-section chat' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, 'Chat:'), span({ class: 'card-value' }, chatKey ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'user-link' }, title || chatKey) : (title || ''))), - displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : '', - category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '', - status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' + div({ class: 'card-field' }, + chatKey + ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'card-value user-link' }, title || chatKey) + : span({ class: 'card-value' }, title || '')), + displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : '', + category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '' ) ); } @@ -1303,7 +1322,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const padTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padTitle || 'Pad') + ':'), span({ class: 'card-value' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'user-link' }, padTitle || padKey) : '')), + div({ class: 'card-field' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'card-value user-link' }, padTitle || padKey) : span({ class: 'card-value' }, padTitle || '')), content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : '' ) ); @@ -1314,8 +1333,41 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const calTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarTitle || 'Calendar') + ':'), span({ class: 'card-value' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'user-link' }, calTitle || calKey) : '')), - content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarStatusLabel || 'Status') + ':'), span({ class: 'card-value' }, content.status)) : '' + div({ class: 'card-field' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'card-value user-link' }, calTitle || calKey) : span({ class: 'card-value' }, calTitle || '')) + ) + ); + } + + if (type === 'poll') { + const options = Array.isArray(content.options) ? content.options : []; + const counts = (action.pollCounts && typeof action.pollCounts === 'object') ? action.pollCounts : {}; + const voters = Number(action.pollVoters) || 0; + const pctOf = (opt) => (voters > 0 ? Math.round(((counts[opt] || 0) / voters) * 100) : 0); + const step = (opt) => Math.round(pctOf(opt) / 5) * 5; + cardBody.push( + div({ class: 'card-section poll' }, + div({ class: 'card-field' }, + span({ class: 'card-value' }, content.question || '') + ), + options.length + ? table({ class: 'poll-results' }, + ...options.map((opt, i) => + tr( + td({ class: 'poll-result-option' }, `${letterOf(i)})`), + td({ class: 'poll-result-bar' }, + div({ class: 'poll-bar' }, + div({ class: `poll-bar-fill poll-bar-fill-${step(opt)} poll-hue-${i % 8}` }) + ) + ), + td({ class: 'poll-result-count' }, `${counts[opt] || 0} (${pctOf(opt)}%)`) + ) + ) + ) + : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, `${i18n.pollVoters}: `), + span({ class: 'card-value' }, String(voters)) + ) ) ); } @@ -1483,6 +1535,51 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } + if (type === 'housing') { + const { title, housing_type, property_type, price, place, status, rooms, size, capacity } = content; + const isFree = String(housing_type || '').toLowerCase() === 'couchsurfing'; + cardBody.push( + div({ class: 'card-section report' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, title) + ), + housing_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingType + ':'), + span({ class: 'card-value' }, String(housing_type).toUpperCase()) + ) : null, + property_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingProperty + ':'), + span({ class: 'card-value' }, String(property_type).toUpperCase()) + ) : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPrice + ':'), + span({ class: 'card-value' }, isFree ? (i18n.housingFree || 'FREE') : `${Number(price || 0).toFixed(2)} ECO`) + ), + place ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPlace + ':'), + span({ class: 'card-value' }, place) + ) : null, + Number(rooms) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingRooms + ':'), + span({ class: 'card-value' }, String(rooms)) + ) : null, + Number(size) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingSize + ':'), + span({ class: 'card-value' }, `${size} m²`) + ) : null, + Number(capacity) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingCapacity + ':'), + span({ class: 'card-value' }, String(capacity)) + ) : null, + status ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingStatus + ':'), + span({ class: 'card-value' }, String(status).toUpperCase()) + ) : null + ) + ); + } + if (type === 'job') { const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content; cardBody.push( @@ -1491,26 +1588,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title) ), - salary && div({ class: 'card-field' }, + salary ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSalary + ':'), span({ class: 'card-value' }, salary + ' ECO') - ), - status && div({ class: 'card-field' }, + ) : null, + status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobStatus + ':'), - span({ class: 'card-value' }, status.toUpperCase()) - ), - job_type && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(status).toUpperCase()) + ) : null, + job_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobType + ':'), - span({ class: 'card-value' }, job_type.toUpperCase()) - ), - location && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(job_type).toUpperCase()) + ) : null, + location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobLocation + ':'), - span({ class: 'card-value' }, location.toUpperCase()) - ), - vacants && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(location).toUpperCase()) + ) : null, + Number(vacants) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), - span({ class: 'card-value' }, vacants) - ), + span({ class: 'card-value' }, String(vacants)) + ) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, @@ -1542,7 +1639,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'parliamentTerm') { - const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content; + const { method, powerType, powerId, powerTitle, startAt, endAt } = content; const powerTypeNorm = String(powerType || '').toLowerCase(); const winnerLink = powerTypeNorm === 'tribe' @@ -1551,17 +1648,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY')) : userLink(powerId, powerTitle); - const methodUpper = String( - i18n['parliamentMethod' + String(method || '').toUpperCase()] || method - ).toUpperCase(); - cardBody.push( div({ class: 'card-section parliament' }, startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '', endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`)) + div({ class: 'card-field' }, span({ class: 'card-value' }, winnerLink)) ) ); } @@ -1576,7 +1667,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - description ? p({ style: 'margin:.4rem 0' }, description) : '', + description ? p({ class: 'card-section-text' }, description) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', @@ -1594,7 +1685,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '', + reasons ? p({ class: 'card-section-text' }, reasons) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', @@ -1615,7 +1706,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '', - description ? p({ style: 'margin:.4rem 0' }, description) : '', + description ? p({ class: 'card-section-text' }, description) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, userLink(proposer))) : '', enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '', @@ -1677,7 +1768,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { return null; } - const detailHref = (type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet') + const detailHref = (type !== 'aiExchange' && type !== 'bankWallet') ? (isParliamentTarget ? `/parliament?filter=${encodeURIComponent(parliamentFilter)}` : isCourtsTarget @@ -1693,7 +1784,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { span({ class: 'pm-exposition-text' }, String(type || '').toUpperCase()) ) ), - renderContentActions(msgId, detailHref) + renderContentActions(msgId, detailHref, { + author: action.author, + reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '', + spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined, + ...favOptsFor(type, msgId, extras) + }) ), ...cardBody, (() => { @@ -1703,27 +1799,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {}; const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${opsTotal})`), + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${opsTotal})`)), div({ class: 'voting-buttons' }, opinionCategories.map(cat => form({ method: 'POST', action: `${routeFn(action.id)}/${cat}` }, - button({ class: 'vote-btn' }, `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${ops[cat] || 0}]`) + button({ class: 'vote-btn' }, `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${ops[cat] || 0}]`) ) ) ) ); })(), - (() => { - const SPREADABLE = new Set([ - 'post','audio','video','image','document','torrent','bookmark', - 'event','calendar','task','votes','vote','market','shop','shopProduct', - 'project','transfer','job','report','industry','industryBuild','industryBlueprint', - 'chat','chatMessage','pad','padEntry','forum','map' - ]); - if (!SPREADABLE.has(type)) return null; - const btn = renderSpreadButton(action.id, spreadMap.get(action.id)); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), (() => { const footerAuthorId = action.author || (content && content.proposer) || ''; return p({ class: 'card-footer' }, @@ -1741,6 +1828,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { return filteredCards; } +const FAV_KIND_BY_TYPE = { + image: 'images', audio: 'audios', video: 'videos', document: 'documents', + bookmark: 'bookmarks', torrent: 'torrents', event: 'events', task: 'tasks', + report: 'reports', votes: 'votes', poll: 'polls', market: 'market', + housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops', + chat: 'chats', chatThread: 'chats', pad: 'pads', calendar: 'calendars', + map: 'maps', forum: 'forum', transfer: 'transfers', post: 'blogs' +}; + +const favOptsFor = (type, id, extras = {}) => { + const favKind = FAV_KIND_BY_TYPE[type]; + if (!favKind) return {}; + const index = extras.favIndex instanceof Map ? extras.favIndex : null; + return { + favKind, + isFavorite: index ? index.get(String(id)) === favKind : false, + returnTo: extras.returnTo || '/activity' + }; +}; + function getViewDetailsAction(type, action) { const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action)); switch (type) { @@ -1762,11 +1869,18 @@ function getViewDetailsAction(type, action) { const link = normalizeSpreadLink(action.content?.spreadTargetId || action.content?.vote?.link || ''); return link ? `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}` : `/activity`; } - case 'post': return `/thread/${id}#${id}`; + case 'post': { + const root = action.content && action.content.root; + return root + ? `/blogs/${encodeURIComponent(root)}#${id}` + : `/blogs/${id}`; + } + case 'feed': return `/feed/${id}`; case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`; case 'votes': return `/votes/${id}`; case 'transfer': return `/transfers/${id}`; case 'pixelia': return `/pixelia`; + case 'poll': return `/polls/${id}`; case 'larpHousePost': return action.content?.house ? `/larp/${encodeURIComponent(action.content.house)}` : `/activity`; case 'tribe': return `/tribe/${id}`; case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`; @@ -1791,6 +1905,7 @@ function getViewDetailsAction(type, action) { case 'chat': return `/chats/${id}`; case 'pad': return `/pads/${id}`; case 'calendar': return `/calendars/${id}`; + case 'housing': return `/housing/${id}`; case 'job': return `/jobs/${id}`; case 'project': return `/projects/${id}`; case 'industry': return `/industry/${id}`; @@ -1805,15 +1920,18 @@ function getViewDetailsAction(type, action) { } } +exports.getViewDetailsAction = getViewDetailsAction; + exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const spreadMap = (extras && extras.spreadMap) || new Map(); const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity; const desc = i18n.activityDesc; const activityTypes = [ - { type: 'recent', label: i18n.typeRecent }, { type: 'all', label: i18n.allButton }, { type: 'mine', label: i18n.mineButton }, + { type: 'recent', label: i18n.typeRecent }, + { type: 'top', label: i18n.typeTop }, { type: 'inhabitants', label: i18n.typeInhabitants }, { type: 'tribe', label: i18n.typeTribe }, { type: 'larp', label: i18n.typeLarp }, @@ -1834,10 +1952,10 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { { type: 'market', label: i18n.typeMarket }, { type: 'project', label: i18n.typeProject }, { type: 'industry', label: i18n.typeIndustry }, + { type: 'housing', label: i18n.typeHousing }, { type: 'job', label: i18n.typeJob }, { type: 'shop', label: i18n.typeShop }, { type: 'transfer', label: i18n.typeTransfer }, - { type: 'curriculum',label: i18n.typeCurriculum }, { type: 'audio', label: i18n.typeAudio }, { type: 'bookmark', label: i18n.typeBookmark }, { type: 'document', label: i18n.typeDocument }, @@ -1846,12 +1964,13 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { { type: 'video', label: i18n.typeVideo } ]; - const FILTER_META = new Set(['recent', 'all', 'mine']); + const FILTER_META = new Set(['all', 'mine', 'recent']); const GROUP_SUBTYPES = { parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'], courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'], banking: ['bankWallet', 'bankClaim', 'ubiClaim'], task: ['task', 'taskAssignment'], + votes: ['votes', 'poll'], shop: ['shop', 'shopProduct'], larp: ['larpHousePost'], inhabitants:['about'], @@ -1896,7 +2015,12 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { } else if (filter === 'industry') { filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone'); } else { - filteredActions = actions.filter(action => (action.type === filter || filter === 'all' || (filter === 'shop' && action.type === 'shopProduct')) && action.type !== 'tombstone'); + filteredActions = actions.filter(action => + (action.type === filter + || filter === 'all' + || (filter === 'shop' && action.type === 'shopProduct') + || (filter === 'votes' && action.type === 'poll')) + && action.type !== 'tombstone'); } const larpEarliest = new Map(); @@ -1993,36 +2117,49 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { h2(i18n.activityList), p(desc) ), - input({ type: 'checkbox', id: 'activity-filter-toggle', class: 'activity-filter-toggle' }), - label({ for: 'activity-filter-toggle', class: 'activity-filter-btn' }, - span(i18n.filter || 'Filter'), span({ class: 'activity-filter-caret' }, '▾')), - div({ class: 'activity-filter-panel' }, - div({ class: 'activity-filter-grid' }, - ...[ - activityTypes.slice(0, 3), - activityTypes.slice(3, 8), - activityTypes.slice(8, 13), - activityTypes.slice(13, 19), - activityTypes.slice(19, 26), - activityTypes.slice(26) - ].map(col => - div({ class: 'activity-filter-col' }, - col.map(({ type, label }) => - form({ method: 'GET', action: '/activity' }, - input({ type: 'hidden', name: 'filter', value: type }), - button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label) - ) + div({ class: 'activity-filter-grid' }, + ...(() => { + const COLUMNS = [ + ['all', 'mine', 'recent', 'top'], + ['inhabitants', 'tribe', 'larp', 'parliament', 'courts'], + ['votes', 'event', 'calendar', 'task', 'report'], + ['banking', 'market', 'housing', 'project', 'industry', 'job', 'shop', 'transfer'], + ['post', 'feed', 'chat', 'pad', 'forum', 'map'], + ['audio', 'bookmark', 'document', 'image', 'torrent', 'video'] + ]; + const byType = new Map(activityTypes.map(t => [t.type, t])); + const placed = new Set(COLUMNS.flat()); + const leftovers = activityTypes.filter(t => !placed.has(t.type)); + return COLUMNS + .map(col => col.map(type => byType.get(type)).filter(Boolean)) + .concat(leftovers.length ? [leftovers] : []) + .filter(col => col.length); + })().map(col => + div({ class: 'activity-filter-col' }, + col.map(({ type, label }) => + form({ method: 'GET', action: '/activity' }, + input({ type: 'hidden', name: 'filter', value: type }), + button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label) ) ) ) - ), - sub - ? div({ class: 'activity-sub-filter' }, - sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase())) - ) - : null + ) ), - section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap)) + sub + ? div({ class: 'activity-sub-filter' }, + sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase())) + ) + : null, + div({ class: 'filters' }, + form({ method: 'GET', action: '/activity', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: qs, placeholder: i18n.activitySearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ) + ), + section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap, extras)) ) ); diff --git a/src/views/market_view.js b/src/views/market_view.js index 83ae5de..e9c70e2 100644 --- a/src/views/market_view.js +++ b/src/views/market_view.js @@ -1,5 +1,7 @@ const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderOpinionsVoting, renderEngagement , renderContentActions } = require("./main_views") +const opinionCategories = require("../backend/opinion_categories") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderUrl } = require("../backend/renderUrl") @@ -95,6 +97,17 @@ const buildReturnTo = (filter, q, minPrice, maxPrice, sort) => { return `/market${params.length ? `?${params.join("&")}` : ""}` } + +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((acc, c) => acc + (Number((opinions || {})[c]) || 0), 0) + +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const total = pos + neg + const full = total > 0 ? Math.round((pos / total) * 5) : 0 + return span({ class: "shop-product-stars" }, "★".repeat(full) + "☆".repeat(5 - full) + ` (${voterCount})`) +} + const renderCardField = (labelText, value = "") => div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...renderUrl(String(value)))) @@ -121,59 +134,12 @@ const renderStockBar = (stockValue, maxValue) => { } const renderMarketCommentsSection = (itemId, returnTo, comments = []) => { - const commentsCount = 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(commentsCount))), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/market/${encodeURIComponent(itemId)}/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 userName = author && author.includes("@") ? author.split("@")[1] : author - const rootId = c.value && c.value.content ? c.value.content.fork || c.value.content.root : null - const text = c.value && c.value.content && c.value.content.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: `/market/${encodeURIComponent(itemId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const isMyBidItem = (item) => { const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] @@ -231,9 +197,9 @@ const renderMarketOwnerActions = (item, returnTo) => { input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "FOR SALE", selected: cur === "FOR SALE" }, i18n.marketFilterForSale), - option({ value: "SOLD", selected: cur === "SOLD" }, i18n.marketFilterSold), - option({ value: "DISCARDED", selected: cur === "DISCARDED" }, i18n.marketFilterDiscarded) + option({ value: "FOR SALE", ...(cur === "FOR SALE" ? { selected: true } : {})}, i18n.marketFilterForSale), + option({ value: "SOLD", ...(cur === "SOLD" ? { selected: true } : {})}, i18n.marketFilterSold), + option({ value: "DISCARDED", ...(cur === "DISCARDED" ? { selected: true } : {})}, i18n.marketFilterDiscarded) ), button({ class: "status-btn project-control-btn", type: "submit" }, i18n.marketActionsChangeStatus) ) @@ -260,21 +226,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { const maxPrice = params.maxPrice const sort = params.sort || "recent" - let title = i18n.marketAllSectionTitle - switch (filter) { - case "mine": - title = i18n.marketMineSectionTitle - break - case "create": - title = i18n.marketCreateSectionTitle - break - case "edit": - title = i18n.marketUpdateSectionTitle - break - case "mybids": - title = i18n.marketFilterMyBids - break - } + const title = i18n.marketTitle let filtered = [] switch (filter) { @@ -337,6 +289,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { ] const isFormMode = filter === "create" || filter === "edit" + const spreadWarning = filter === "edit" ? await renderSpreadEditWarning(itemEdit && (itemEdit.id || itemEdit.key)) : null return template( title, @@ -347,18 +300,18 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { form( { method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" }, ...hiddenCtx, - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine), - button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems), - button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions), - button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids), - button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew), - button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed), - button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken), - button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale), - button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold), - button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase()), + button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase()), + button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase()), + button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase()), + button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase()), + button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase()), + button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase()), + button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase()), + button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase()), + button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase()), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton) ) ), @@ -394,9 +347,9 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { ), select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.marketSortRecent), - option({ value: "price", selected: sort === "price" }, i18n.marketSortPrice), - option({ value: "deadline", selected: sort === "deadline" }, i18n.marketSortDeadline) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.marketSortRecent), + option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.marketSortPrice), + option({ value: "deadline", ...(sort === "deadline" ? { selected: true } : {})}, i18n.marketSortDeadline) ), button({ type: "submit", class: "filter-box__button" }, i18n.marketSearchButton) ) @@ -408,6 +361,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { isFormMode ? div( { class: "market-form" }, + spreadWarning, form( { action: filter === "edit" ? `/market/update/${encodeURIComponent(itemEdit.id)}` : "/market/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: "/market?filter=mine" }), @@ -416,14 +370,14 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { br(), select( { name: "item_type", id: "item_type", required: true }, - option({ value: "auction", selected: itemEdit && itemEdit.item_type === "auction" }, "Auction"), - option({ value: "exchange", selected: itemEdit && itemEdit.item_type === "exchange" }, "Exchange") + option({ value: "auction", ...(itemEdit && itemEdit.item_type === "auction" ? { selected: true } : {})}, "Auction"), + option({ value: "exchange", ...(itemEdit && itemEdit.item_type === "exchange" ? { selected: true } : {})}, "Exchange") ), br(), br(), label(i18n.marketItemTitle), br(), - input({ type: "text", name: "title", id: "title", value: (itemEdit && itemEdit.title) || params.title || "", required: true }), + input({ type: "text", name: "title", maxlength: "100", id: "title", value: (itemEdit && itemEdit.title) || params.title || "", required: true }), br(), br(), label(i18n.marketItemDescription), @@ -440,9 +394,9 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { br(), select( { name: "item_status", id: "item_status" }, - option({ value: "BROKEN", selected: itemEdit && itemEdit.item_status === "BROKEN" }, "BROKEN"), - option({ value: "USED", selected: itemEdit && itemEdit.item_status === "USED" }, "USED"), - option({ value: "NEW", selected: itemEdit && itemEdit.item_status === "NEW" }, "NEW") + option({ value: "BROKEN", ...(itemEdit && itemEdit.item_status === "BROKEN" ? { selected: true } : {})}, "BROKEN"), + option({ value: "USED", ...(itemEdit && itemEdit.item_status === "USED" ? { selected: true } : {})}, "USED"), + option({ value: "NEW", ...(itemEdit && itemEdit.item_status === "NEW" ? { selected: true } : {})}, "NEW") ), br(), br(), @@ -460,8 +414,8 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { br(), select( { name: "visibility" }, - option({ value: "PUBLIC", selected: (itemEdit?.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: itemEdit?.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") + option({ value: "PUBLIC", ...((itemEdit?.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(itemEdit?.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") ), br(), br(), @@ -541,10 +495,15 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { renderMediaBlob(item.image, '/assets/images/default-market.png', { class: 'tribe-card-hero-image' }) ) ), + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, item.title)) ), + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), div({ class: "card-chips-row" }, renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()), item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null, @@ -553,18 +512,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(item.lifetime, i18n) ), - div({ class: "market-card-price card-date-highlight" }, `${item.price} ECO`), - div({ class: "card-visit-btn-centered" }, - form({ method: 'GET', action: `/market/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - input({ type: "hidden", name: "q", value: q }), - input({ type: "hidden", name: "minPrice", value: String(minPrice ?? "") }), - input({ type: "hidden", name: "maxPrice", value: String(maxPrice ?? "") }), - input({ type: "hidden", name: "sort", value: sort }), - button({ type: 'submit', class: 'filter-btn' }, i18n.viewItem || i18n.marketVisitItem || 'View Item') - ) - ) + div({ class: "market-card-price card-date-highlight" }, `${item.price} ECO`) ) ) }) @@ -589,6 +537,7 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { return template( item.title, + section(div({ class: "tags-header" }, h2(i18n.marketTitle), p(i18n.marketDescription))), section( div( { class: "filters" }, @@ -598,18 +547,18 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }), input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine), - button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems), - button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions), - button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids), - button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew), - button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed), - button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken), - button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale), - button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold), - button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase()), + button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase()), + button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase()), + button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase()), + button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase()), + button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase()), + button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase()), + button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase()), + button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase()), + button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase()), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton) ) ), @@ -637,26 +586,13 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { )) pushRow(i18n.marketItemSeller, userLink(item.seller)) if (item.shopId && item.shopTitle) pushRow(i18n.marketShopLabel || "Shop", a({ href: `/shops/${encodeURIComponent(item.shopId)}`, class: "user-link" }, item.shopTitle)) - if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm:ss")) - - const itemSide = div({ class: "tribe-side" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, item.title) - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(item.id, params.spreads)), - renderMediaBlob(item.image, "/assets/images/default-market.png"), - div({ class: "card-date-highlight" }, `${item.price} ECO`), - renderStockBar(item.stock, maxStock), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), - tagsNode - ) + if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm")) const visibilityRow = String(item.seller) === String(userId) ? (() => { const vis = isHidden ? 'HIDDEN' : 'PUBLIC' const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC' - return div({ class: "tribe-side-actions" }, + return div({ class: "tribe-side-actions housing-status-row" }, span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), renderVisibilityChip(vis, i18n), form({ method: "POST", action: `/market/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" }, @@ -670,12 +606,6 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { : null const marketActions = [] - if (item.seller && String(item.seller) !== String(userId)) { - marketActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: item.seller }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) - } if (String(item.seller) === String(userId)) { marketActions.push(form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` }, button({ type: "submit", class: "update-btn" }, i18n.marketUpdateButton || "Update") @@ -684,9 +614,26 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { button({ type: "submit", class: "delete-btn" }, i18n.marketDeleteButton || "Delete") )) } - const itemMain = div({ class: "tribe-main" }, - marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null, + + const itemSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(item.id, null, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, item.title) + ), + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderMediaBlob(item.image, "/assets/images/default-market.png"), + div({ class: "card-date-highlight" }, `${item.price} ECO`), + renderStockBar(item.stock, maxStock), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + tagsNode, visibilityRow, + marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null + ) + + const itemMain = div({ class: "tribe-main" }, item.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.marketItemDescription), @@ -704,7 +651,7 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { { class: "auction-bid-table" }, tr(th(i18n.marketAuctionBidTime), th(i18n.marketAuctionUser), th(i18n.marketAuctionBidAmount)), parsedBids.map((bid) => - tr(td(moment(bid.time).format("YYYY-MM-DD HH:mm:ss")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`)) + tr(td(moment(bid.time).format("YYYY/MM/DD HH:mm")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`)) ) ) : p({ class: "tribe-side-description" }, i18n.marketNoBids || "No bids yet"), @@ -729,7 +676,12 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { ) ) : null, - renderMarketCommentsSection(item.id, returnTo, comments) + renderEngagement(item.id, + String(item.seller) !== String(userId) && item.purchasedByViewer + ? renderOpinionsVoting("/market/opinions", item.id, item.opinions, returnTo, item.opinions_inhabitants) + : null, + renderMarketCommentsSection(item.id, returnTo, comments) + ) ) return div({ class: "tribe-details" }, itemSide, itemMain) diff --git a/src/views/tags_view.js b/src/views/tags_view.js index 2976f5b..32c9a17 100644 --- a/src/views/tags_view.js +++ b/src/views/tags_view.js @@ -1,14 +1,12 @@ -const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody, input } = require("../server/node_modules/hyperaxe"); const { template, i18n } = require('./main_views'); const getFilteredTags = (filter, tags) => { - let filteredTags = Array.isArray(tags) ? tags : []; - if (filter === 'top') { - filteredTags = filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); - } else { - filteredTags = filteredTags.sort((a, b) => a.name.localeCompare(b.name)); - } - return filteredTags; + const filteredTags = Array.isArray(tags) ? [...tags] : []; + if (filter === 'top') return filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); + if (filter === 'mine') return filteredTags.sort((a, b) => (b.mine || 0) - (a.mine || 0) || a.name.localeCompare(b.name)); + if (filter === 'recent') return filteredTags.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0)); + return filteredTags.sort((a, b) => a.name.localeCompare(b.name)); }; const renderTagsTable = (filteredTags) => { @@ -56,13 +54,11 @@ const renderTagsCloud = (mergedTags) => { ); }; -exports.tagsView = async (tags, filter) => { +exports.tagsView = async (tags, filter, search = '') => { const filteredTags = getFilteredTags(filter, tags); + const query = String(search || '').trim(); - const title = - filter === 'top' ? i18n.tagsTopSectionTitle : - filter === 'cloud' ? i18n.tagsCloudSectionTitle : - i18n.tagsAllSectionTitle; + const title = i18n.tagsTitle; return template( title, @@ -72,15 +68,25 @@ exports.tagsView = async (tags, filter) => { p(i18n.tagsDescription) ), div({ class: 'filters' }, - form({ method: 'GET', action: '/tags' }, - button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterAll), - button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterTop), - button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterCloud) + form({ method: 'GET', action: '/tags', class: 'ui-toolbar ui-toolbar--filters' }, + input({ type: 'hidden', name: 'search', value: query }), + button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterAll).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'mine', class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterMine).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'recent', class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterRecent).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterTop).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterCloud).toUpperCase()) + ) + ), + div({ class: 'tags-search' }, + form({ method: 'GET', action: '/tags', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter || 'all' }), + input({ type: 'text', name: 'search', value: query, placeholder: i18n.tagsSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)) ) ), div({ class: 'tags-list' }, filteredTags.length === 0 - ? p(i18n.tagsNoItems) + ? p(query ? i18n.noResultsFound : i18n.tagsNoItems) : filter !== 'cloud' ? renderTagsTable(filteredTags) : renderTagsCloud(filteredTags) diff --git a/src/views/vote_view.js b/src/views/vote_view.js index 557c64f..9a77013 100644 --- a/src/views/vote_view.js +++ b/src/views/vote_view.js @@ -1,5 +1,6 @@ const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl"); @@ -13,15 +14,15 @@ const voteLabel = (opt) => i18n["vote" + opt.split("_").map(w => w.charAt(0) + w.slice(1).toLowerCase()).join("")] || opt; const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => { - const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", color: "#ffcc00" }; + const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", variant: "pending" }; if (totalVotesNum < VOTE_QUORUM) return noResult; const opts = voteOptions.filter((o) => o !== "FOLLOW_MAJORITY"); const maxCount = opts.reduce((m, o) => Math.max(m, baseCounts[o] || 0), 0); const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount); if (maxCount === 0 || topOpts.length > 1) return noResult; const winner = topOpts[0]; - const color = winner === "YES" ? "#4caf50" : winner === "NO" ? "#e53935" : "#ffcc00"; - return { text: voteLabel(winner), color }; + const variant = winner === "YES" ? "yes" : winner === "NO" ? "no" : "pending"; + return { text: voteLabel(winner), variant }; }; const normalizeStatus = (v) => { @@ -110,7 +111,7 @@ const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) => return div({ class: "trending-card vote-card" + (isOwn ? " own-content" : "") }, div({ class: "card-header activity-card-header" }, span(), - renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`) + renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: spreadInfo || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question }) ), div({ class: "card-section vote-card-body" }, div({ class: "shop-title-row" }, @@ -123,8 +124,7 @@ const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) => div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), - div({ class: "job-meta-line", style: `color:${outcome.color};font-weight:bold;` }, `${i18n.voteResults || "Results"}: ${outcome.text}`), - div({ class: "card-spread-centered" }, renderSpreadButton(v.id, spreadInfo)) + div({ class: `job-meta-line vote-outcome vote-outcome-${outcome.variant}` }, `${i18n.voteResults || "Results"}: ${outcome.text}`) ) ); }; @@ -147,12 +147,6 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti ].filter(Boolean); const sideActions = []; - if (v.createdBy && v.createdBy !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: v.createdBy }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } for (const a of renderVoteOwnerActions(v, returnTo, mode || "")) sideActions.push(a); const cleanTags = (Array.isArray(v.tags) ? v.tags : []).filter((t) => t && !String(t).includes(":")); @@ -172,17 +166,20 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti pushRow(i18n.voteStatus, statusLabel(v.status)); const voteSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(v.id, null, { spread: params.spreads || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, v.question) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(v.id, params.spreads)), table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + renderDocumentActions('votes', v.id) ); const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo); @@ -197,7 +194,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti div({ class: "job-section" }, h2({ class: "job-section-title" }, `${i18n.voteResults || "Results"}: `, - span({ style: `color:${outcome.color} !important;font-weight:bold;` }, outcome.text) + span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text) ), div({ class: "vote-table" }, table( @@ -210,71 +207,20 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), userLink(v.createdBy) ), - renderOpinionsBar(v, returnTo) + renderEngagement(v.id, renderOpinionsBar(v, returnTo), + mode === "detail" ? renderCommentsSection(v.id, params.comments || [], activeFilter) : null) ); return div({ class: "tribe-details" }, voteSide, voteMain); }; const renderCommentsSection = (voteId, comments, activeFilter) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/votes/${encodeURIComponent(voteId)}?filter=${encodeURIComponent(activeFilter || "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: `/votes/${encodeURIComponent(voteId)}/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) - ) - ), - comments && comments.length - ? div( - { class: "comments-list" }, - comments.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: `/votes/${encodeURIComponent(voteId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => { @@ -285,14 +231,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, ? activeFilterParam : (standardFilters.includes(mode) ? mode : "all"); - const title = - mode === "mine" ? i18n.voteMineSectionTitle : - mode === "create" ? i18n.voteCreateSectionTitle : - mode === "edit" ? i18n.voteUpdateSectionTitle : - mode === "open" ? i18n.voteOpenTitle : - mode === "closed" ? i18n.voteClosedTitle : - mode === "detail" ? (i18n.voteDetailSectionTitle || i18n.voteAllSectionTitle) : - i18n.voteAllSectionTitle; + const title = i18n.votationsTitle; const voteToEdit = list.find((v) => v.id === voteId) || {}; const editTags = Array.isArray(voteToEdit.tags) ? voteToEdit.tags.filter(Boolean) : []; @@ -317,8 +256,19 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, const listReturnTo = standardFilters.includes(activeFilter) ? `/votes?filter=${encodeURIComponent(activeFilter)}` : "/votes"; - const deadlineMin = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); - const deadlineValue = voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""; + const minVoteDays = Number(params.minVoteDays) > 0 ? Number(params.minVoteDays) : 7; + const deadlineMin = moment().add(minVoteDays, "days").format("YYYY-MM-DDTHH:mm"); + const draft = params.draft || {}; + const deadlineValue = draft.deadline + ? String(draft.deadline) + : (voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""); + const questionValue = draft.question !== undefined && draft.question !== "" ? String(draft.question) : (voteToEdit.question || ""); + const tagsValue = draft.tags ? String(draft.tags) : editTags.join(", "); + const formError = params.error === "deadline" + ? String(i18n.voteErrorDeadlineMin || "The deadline must be at least {days} days from now.").replace("{days}", String(minVoteDays)) + : params.error + ? (i18n.voteErrorGeneric || "The vote could not be saved.") + : ""; return template( title, @@ -328,23 +278,41 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, { class: "filters" }, form( { method: "GET", action: "/votes" }, - button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterMine), - button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterOpen), - button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterClosed), + button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterAll).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterMine).toUpperCase()), + button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterOpen).toUpperCase()), + button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterClosed).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: mode === "create" ? "create-button active" : "create-button" }, i18n.voteCreateButton) ) - ) + ), + mode !== "create" && mode !== "edit" + ? div({ class: "filters" }, + form({ method: "GET", action: "/votes", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: mode }), + input({ type: "text", name: "q", value: (params && params.q) || "", placeholder: i18n.votesSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + : null ), section( (mode === "edit" || mode === "create") ? div( { class: "vote-form" }, + mode === "edit" ? await renderSpreadEditWarning(voteId) : null, + formError + ? div({ class: "error-box vote-form-error" }, + p({ class: "error-title" }, i18n.voteErrorTitle || "Check the form"), + p(formError) + ) + : null, form( { action: mode === "edit" ? `/votes/update/${encodeURIComponent(voteId)}` : "/votes/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: listReturnTo }), h2(i18n.voteQuestionLabel), - input({ type: "text", name: "question", id: "question", required: true, value: voteToEdit.question || "" }), br(), br(), + input({ type: "text", name: "question", id: "question", required: true, value: questionValue }), br(), br(), label(i18n.voteDeadlineLabel), br(), input({ type: "datetime-local", @@ -355,16 +323,15 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, value: deadlineValue }), br(), br(), label(i18n.voteTagsLabel), br(), - input({ type: "text", name: "tags", id: "tags", value: editTags.join(", ") }), br(), br(), + input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(), button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton) ) ) : mode === "detail" && voteId - ? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, params) + ? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, { ...params, comments }) : filtered.length > 0 ? div({ class: "jobs-grid" }, filtered.map((v) => renderVoteListItem(v, voteOptions, activeFilter, params.spreadMap && params.spreadMap.get(v.id)))) : p(i18n.novotes), - (mode === "detail" && voteId) ? renderCommentsSection(voteId, comments, activeFilter) : null ) ); };