fix: cerrar la integracion 0.9.5 — recursos que faltaban y regresiones
Una revision a fondo del arbol contra 0.9.5 saco varios fallos, unos heredados y
otros que introdujo la propia integracion. Verificados uno a uno antes de tocar.
Recursos referenciados que no existian (los introduje al adoptar vistas de 0.9.5
que los usan, mientras la rama los habia borrado):
- Los cuatro temas *-SNH.css. settings_view ofrece Dark, Clear, Matrix y Purple,
pero solo estaba OasisMobile: elegir cualquiera daba 404 y la app se quedaba sin
estilos. Ahora los cuatro responden 200.
- pdf.min.mjs, pdf.worker.min.mjs y pdf-viewer.js, referenciados desde seis vistas.
Regresiones revertidas a 0.9.5 verbatim en catorce ficheros. Las de fondo:
- larp_model perdio getGoverningPeriodId, que backend.js llama: el anuncio de
gobierno no salia nunca, con el TypeError tragado por un catch.
- courts_view y search_view tenian selected: false, que hyperscript SI serializa
como atributo y HTML interpreta como seleccionado. La forma de upstream, el
spread condicional, es la correcta.
- pm_model perdio includeDeleted, projects_model la validacion de deadline pasado,
jobs_model el desempate por timestamp, middleware el flag de debug HTTP.
forum_view pasa a 0.9.5 conservando el filtro hot. Esto arregla un fallo activo: la
vista de la rama declaraba un cuarto parametro topic y backend.js llama con cinco
argumentos, asi que al abrir una respuesta el %clave del hilo se colaba como tema y
el hilo salia VACIO. Upstream pasa los mismos cinco argumentos a una firma de tres y
JS los ignora, que es lo correcto.
chats_view pasa a 0.9.5 y se implementa replyTo de verdad. Estaba muerto de punta a
punta: backend.js no leia ctx.query.replyTo ni pasaba el cuarto argumento a
sendMessage, asi que ningun mensaje llegaba a tener replyTo y la cita no se
renderizaba jamas. Ahora la cadena esta completa —modelo, GET, POST y vista— con
indice msgById armado antes de mezclar con las encuestas, y sin gate de movil:
responder a un mensaje no es una funcion de movil.
aria-label: encontrada la causa. No es que hyperaxe lo ignore, es que hyperscript
solo asigna como propiedad y html-element solo serializa atributos de su tabla, en
la que aria-* no esta; data-* si tiene rama propia, por eso las tablas responsive
funcionaban. Se arregla con el escape hatch attrs:{}, sin tocar node_modules.
prepare.sh copia README.md y falla si no esta: backend.js lo lee con readFileSync
sin try/catch, de modo que sin el fichero el backend muere con ENOENT al arrancar.
Verificado con la app en marcha: 17 rutas OK, los cuatro recursos que faltaban
sirviendo 200, aria-label emitido en el HTML, e interfaz intacta (10 categorias,
50 modulos, 7 accesos rapidos, 0 etiquetas vacias).
This commit is contained in:
parent
7459644a73
commit
c3a1c841e6
27 changed files with 1994 additions and 220 deletions
|
|
@ -63,7 +63,10 @@ fi
|
|||
|
||||
mkdir -p "$work/nodejs-project"
|
||||
cp -r "$REPO/src" "$work/nodejs-project/"
|
||||
cp "$REPO/main.js" "$REPO/package.json" "$work/nodejs-project/" 2>/dev/null || true
|
||||
# backend.js lee README.md con readFileSync SIN try/catch (linea ~1690):
|
||||
# si falta, el backend muere con ENOENT al arrancar.
|
||||
cp "$REPO/main.js" "$REPO/package.json" "$REPO/README.md" "$work/nodejs-project/" 2>/dev/null || true
|
||||
test -f "$work/nodejs-project/README.md" || { echo "FALTA README.md: el backend no arrancaria"; exit 1; }
|
||||
rm -rf "$work/nodejs-project/src/server/node_modules"
|
||||
mv "$work/_nm" "$work/nodejs-project/src/server/node_modules"
|
||||
|
||||
|
|
|
|||
|
|
@ -5131,7 +5131,7 @@ router
|
|||
allChats = (await chatsModel.listAll({ filter: 'all', q: '', viewerId: uid })).filter(x => !x.tribeId);
|
||||
} catch (_) { allChats = []; }
|
||||
}
|
||||
ctx.body = await singleChatView({ ...chat, isFavorite: fav.has(String(chat.rootId || chat.key)), isTribeMember }, filter, messages, { q, chatList: forkMobileChats ? allChats : [], polls: chatPolls, pollsEnabled, returnTo: safeReturnTo(ctx, `/chats?filter=${encodeURIComponent(filter)}`, ['/chats']), spreads: await spreads.forMessage(chat.key).catch(() => null), workspace: uxChats, allChats });
|
||||
ctx.body = await singleChatView({ ...chat, isFavorite: fav.has(String(chat.rootId || chat.key)), isTribeMember }, filter, messages, { q, replyTo: String(ctx.query.replyTo || ''), chatList: forkMobileChats ? allChats : [], polls: chatPolls, pollsEnabled, returnTo: safeReturnTo(ctx, `/chats?filter=${encodeURIComponent(filter)}`, ['/chats']), spreads: await spreads.forMessage(chat.key).catch(() => null), workspace: uxChats, allChats });
|
||||
})
|
||||
.get("/pads", async (ctx) => {
|
||||
if (!checkMod(ctx, 'padsMod')) { ctx.redirect('/modules'); return; }
|
||||
|
|
@ -8232,7 +8232,8 @@ router
|
|||
const imageBlob = ctx.request.files?.image ? extractBlobId(await handleBlobUpload(ctx, 'image')) : null;
|
||||
if (!text && !imageBlob) { ctx.redirect(`/chats/${encodeURIComponent(ctx.params.chatId)}`); return; }
|
||||
try {
|
||||
await chatsModel.sendMessage(ctx.params.chatId, text, imageBlob);
|
||||
const replyTo = String(ctx.request.body.replyTo || '').trim() || null;
|
||||
await chatsModel.sendMessage(ctx.params.chatId, text, imageBlob, replyTo);
|
||||
} catch (err) {
|
||||
if (err && err.code === 'CHAT_RATE_LIMIT') {
|
||||
const { i18n } = require('../views/main_views');
|
||||
|
|
|
|||
372
src/client/assets/themes/Clear-SNH.css
Normal file
372
src/client/assets/themes/Clear-SNH.css
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
body {
|
||||
background-color: #F9F9F9 !important;
|
||||
color: #2C2C2C !important;
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
background-color: #FFFFFF !important;
|
||||
border: 1px solid #E0E0E0 !important;
|
||||
}
|
||||
|
||||
button, input[type="submit"], input[type="button"] {
|
||||
background-color: #FF6F00 !important;
|
||||
color: #FFFFFF !important;
|
||||
border: none !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 10px 20px !important;
|
||||
cursor: pointer !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
button:hover, input[type="submit"]:hover, input[type="button"]:hover {
|
||||
background-color: #FF8F00 !important;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #2C2C2C !important;
|
||||
border: 1px solid #E0E0E0 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 8px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007BFF !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
p {
|
||||
color: black !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.created-at, .about-time, .time {
|
||||
font-size: 0.9rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #2C2C2C !important;
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #F8F8F8 !important;
|
||||
padding: 12px 15px !important;
|
||||
text-align: left !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #FAFAFA !important;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 12px 15px !important;
|
||||
}
|
||||
|
||||
.profile {
|
||||
background-color: #FFFFFF !important;
|
||||
padding: 20px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.profile .name {
|
||||
color: #FF6F00 !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 3px solid #FF6F00 !important;
|
||||
border-radius: 50% !important;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
}
|
||||
|
||||
article, section {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #2C2C2C !important;
|
||||
padding: 20px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.post-preview img {
|
||||
border-radius: 8px !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: #FFFFFF !important;
|
||||
border: 1px solid #E0E0E0 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #B0B0B0 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #F9F9F9 !important;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
background-color: #FFFFFF !important;
|
||||
border-color: #E0E0E0 !important;
|
||||
color: #2C2C2C !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #FFFFFF !important;
|
||||
border-top: 1px solid #E0E0E0 !important;
|
||||
padding: 15px 0 !important;
|
||||
}
|
||||
|
||||
footer a {
|
||||
background-color: #007BFF !important;
|
||||
color: #FFFFFF !important;
|
||||
padding: 10px 20px !important;
|
||||
border-radius: 6px !important;
|
||||
text-decoration: none !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
background-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul,
|
||||
.sidebar-right nav ul {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul,
|
||||
.sidebar-right nav ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li,
|
||||
.sidebar-right nav ul li {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li a,
|
||||
.sidebar-right nav ul li a,
|
||||
.header nav ul li a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
background-color: #ffffff !important;
|
||||
color: #2C2C2C !important;
|
||||
border: 1px solid #D0D0D0 !important;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li a:hover,
|
||||
.sidebar-right nav ul li a:hover,
|
||||
.header nav ul li a:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.filter-btn,
|
||||
.create-button,
|
||||
.edit-btn,
|
||||
.delete-btn,
|
||||
.join-btn,
|
||||
.leave-btn,
|
||||
.buy-btn {
|
||||
background-color: #FF6F00 !important;
|
||||
color: #FFFFFF !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.filter-btn:hover,
|
||||
.create-button:hover {
|
||||
background-color: #FF8F00 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
color: #4A4A4A;
|
||||
box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.1);
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #2D2D2D;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
color: #6C6C6C;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.card-field {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link {
|
||||
color: #181818;
|
||||
background: #D94F4F;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link:hover {
|
||||
background: #D94F4F;
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link {
|
||||
background-color: #FFD600;
|
||||
color: #FFFFFF;
|
||||
border-color: #FFD600;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
a.user-link:hover {
|
||||
background-color: #FFD600;
|
||||
border-color: #FFD600;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link:focus {
|
||||
background-color: #9A2F2F;
|
||||
border-color: #9A2F2F;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.date-link {
|
||||
background-color: #2F3C32;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-link:hover {
|
||||
background-color: #3E4A3D;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.activitySpreadInhabitant2 {
|
||||
background-color: #3E4A3D;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.activityVotePost {
|
||||
background-color: #3B5C42;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.update-banner {
|
||||
background-color: #FFF3E0 !important;
|
||||
border-bottom-color: #FFE0B2 !important;
|
||||
color: #E65100 !important;
|
||||
}
|
||||
|
||||
.update-banner-link {
|
||||
color: #FF6F00 !important;
|
||||
}
|
||||
|
||||
.snh-invite-code {
|
||||
color: #007BFF !important;
|
||||
}
|
||||
|
||||
.carbon-bar-track {
|
||||
background: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.carbon-bar-max {
|
||||
background: #cc7700 !important;
|
||||
}
|
||||
|
||||
.stats-kpi-label { color: #2D2D2D !important; }
|
||||
.stats-kpi-value { color: #007BFF !important; }
|
||||
.carbon-bar-note, .carbon-bar-formula { color: #007BFF !important; }
|
||||
.graphos-node-label { fill: #2C2C2C !important; }
|
||||
.graphos-node-label-me { fill: #cc7700 !important; }
|
||||
.data-best-label { color: #cc7700 !important; }
|
||||
.data-best-card { background: #FFF3E0 !important; border: 1px solid rgba(204, 119, 0, .4) !important; }
|
||||
.graphos-legend { color: #2C2C2C !important; }
|
||||
.graphos-edge { stroke: #BBB !important; }
|
||||
.graphos-edge-discovered { stroke: #b8860b !important; }
|
||||
.graphos-edge-unknown { stroke: #999 !important; }
|
||||
.graphos-node-circle-discovered { fill: #ffd700 !important; stroke: #b8860b !important; }
|
||||
.graphos-legend-dot.graphos-node-circle-discovered { background: #ffd700 !important; border-color: #b8860b !important; }
|
||||
/* Blockexplorer */
|
||||
.blockchain-view { background-color: #F4F4F4 !important; color: #2C2C2C !important; }
|
||||
.block { background: #FFFFFF !important; box-shadow: 0 2px 12px rgba(0,0,0,0.06) !important; }
|
||||
.block:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.10) !important; }
|
||||
.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #2D2D2D !important; }
|
||||
.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #007BFF !important; }
|
||||
.block-content-preview, .block-content { background: #F8F8F8 !important; color: #2C2C2C !important; }
|
||||
.block-author { color: #FF6F00 !important; background: rgba(255,111,0,0.08) !important; }
|
||||
.block-author:hover { color: #FF8F00 !important; }
|
||||
.block-url { color: #007BFF !important; }
|
||||
.block-row--details .block-url { background: #F0F0F0 !important; }
|
||||
.block-row--details .block-url:hover { background: #E0E0E0 !important; color: #FF6F00 !important; }
|
||||
.btn-singleview { background: #F0F0F0 !important; color: #FF6F00 !important; }
|
||||
.btn-singleview:hover { background: #E0E0E0 !important; color: #FF8F00 !important; }
|
||||
.btn-spread-on, .content-actions .btn-pin-on { background: #FF6F00 !important; color: #FFFFFF !important; }
|
||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #FF8F00 !important; color: #FFFFFF !important; }
|
||||
.btn-back { background: #FF6F00 !important; color: #FFFFFF !important; }
|
||||
.btn-back:hover { background: #FF8F00 !important; color: #FFFFFF !important; }
|
||||
.block-info-table td, .pm-info-table td { border-color: #E0E0E0 !important; }
|
||||
.block-diagram { border-color: #E0E0E0 !important; background: #FFFFFF !important; }
|
||||
.block-diagram-ruler { color: #2D2D2D !important; background: #F8F8F8 !important; border-bottom-color: #E0E0E0 !important; }
|
||||
.block-diagram-ruler span { color: #2D2D2D !important; }
|
||||
.block-diagram-cell { border-color: #E0E0E0 !important; background: #FFFFFF !important; }
|
||||
.bd-label { color: #2D2D2D !important; }
|
||||
.bd-value { color: #007BFF !important; }
|
||||
.deleted-label { color: #D32F2F !important; }
|
||||
|
||||
/* Tribes */
|
||||
.tribe-card { background: #FFFFFF !important; border-color: #E0E0E0 !important; }
|
||||
.tribe-card:hover { border-color: #007BFF !important; }
|
||||
.tribe-card-title { color: #2D2D2D !important; }
|
||||
.tribe-card-description { color: #555 !important; }
|
||||
.tribe-info-table td { border-color: #E0E0E0 !important; }
|
||||
.tribe-info-label { color: #2D2D2D !important; background: #FFFFFF !important; }
|
||||
.tribe-info-value { color: #007BFF !important; background: #FFFFFF !important; }
|
||||
.tribe-info-empty { color: #999 !important; }
|
||||
.tribe-card-members { border-color: #E0E0E0 !important; background: #F8F8F8 !important; }
|
||||
.tribe-members-count { color: #FF6F00 !important; }
|
||||
.tribe-card-actions { border-color: #E0E0E0 !important; background: #F8F8F8 !important; }
|
||||
.tribe-action-btn { border-color: #FF6F00 !important; color: #FF6F00 !important; }
|
||||
.tribe-action-btn:hover { background: #FF6F00 !important; color: #fff !important; }
|
||||
.tribe-subtribe-link { background: #F0F0F0 !important; border-color: #E0E0E0 !important; color: #FF6F00 !important; }
|
||||
.tribe-thumb-link { border-color: #E0E0E0 !important; }
|
||||
.tribe-thumb-link:hover { border-color: #FF6F00 !important; }
|
||||
.tribe-subtribe-link:hover { background: #E0E0E0 !important; }
|
||||
.tribe-parent-image { border-color: #E0E0E0 !important; }
|
||||
.tribe-parent-box { background: #FFFFFF !important; }
|
||||
.chat-message { background: #FFFFFF !important; border: 1px solid #E0E0E0 !important; }
|
||||
.chat-bubble-row-self .chat-message { background: #FFF3E0 !important; border-color: rgba(204, 119, 0, .35) !important; }
|
||||
.chat-message-author { background: #FDF6EC !important; }
|
||||
.chat-bubble-sender { color: #1565C0 !important; }
|
||||
.chat-bubble-sender-owner { color: #cc7700 !important; }
|
||||
.chat-bubble-time { color: #999 !important; }
|
||||
.chat-day-chip { background: #F0F0F0 !important; color: #666 !important; border-color: #DDD !important; }
|
||||
.chat-topics { background: #FAFAFA !important; border-color: #E0E0E0 !important; }
|
||||
.chat-topic:hover { background: #F0F0F0 !important; }
|
||||
.chat-topic-active { background: #FFF3E0 !important; }
|
||||
.chat-topic-title { color: #2D2D2D !important; }
|
||||
.chat-workspace-empty { border-color: #DDD !important; }
|
||||
364
src/client/assets/themes/Dark-SNH.css
Normal file
364
src/client/assets/themes/Dark-SNH.css
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
body {
|
||||
background-color: #121212;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
background-color: #1F1F1F;
|
||||
}
|
||||
|
||||
.sidebar-left, .sidebar-right {
|
||||
background-color: #1A1A1A;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
background-color: #1C1C1C;
|
||||
}
|
||||
|
||||
button, input[type="submit"], input[type="button"] {
|
||||
background-color: #444;
|
||||
color: #FFD700;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
button:hover, input[type="submit"]:hover, input[type="button"]:hover {
|
||||
background-color: #333;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
background-color: #333;
|
||||
color: #FFD700;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #FFDD44;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: #222;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
nav ul li a:hover {
|
||||
color: #FFDD44;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.profile {
|
||||
background-color: #222;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.profile .name {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 3px solid #FFD700;
|
||||
}
|
||||
|
||||
article, section {
|
||||
background-color: #1C1C1C;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.article img {
|
||||
border: 3px solid #FFD700;
|
||||
}
|
||||
|
||||
.post-preview img {
|
||||
border: 3px solid #FFD700;
|
||||
}
|
||||
|
||||
.post-preview .image-container {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: #1A1A1A;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
div .header-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
background-color: #1A1A1A;
|
||||
border: 1px solid #333;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #1F1F1F;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
footer a {
|
||||
background-color: #444;
|
||||
color: #FFD700;
|
||||
text-align: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
background-color: #333;
|
||||
color: #FFDD44;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 16px;
|
||||
padding: 0px 24px 10px 24px;
|
||||
margin-bottom: 16px;
|
||||
color: #FFD600;
|
||||
font-family: inherit;
|
||||
box-shadow: 0 2px 20px 0 #FFD60024;
|
||||
}
|
||||
|
||||
.card-section {
|
||||
border:none;
|
||||
padding: 10px 0 0 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0px;
|
||||
margin-top: 8px;
|
||||
padding-top: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #ffa300;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1.5px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 6px;
|
||||
font-weight: 500;
|
||||
color: #ff9900;
|
||||
font-size: 1.07em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
margin-bottom: 6;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-top: 0;
|
||||
margin-bottom: 4;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-field {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
margin: 5px 0 3px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link {
|
||||
text-decoration: none;
|
||||
color: #181818;
|
||||
background: #FFD600;
|
||||
padding: 5px 13px 4px 13px;
|
||||
border-radius: 7px;
|
||||
font-size: .98em;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link:hover {
|
||||
background: #ffe86a;
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link {
|
||||
background-color: #FFA500;
|
||||
color: #000;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
border: 2px solid transparent;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
a.user-link:hover {
|
||||
background-color: #FFD700;
|
||||
border-color: #FFD700;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link:focus {
|
||||
background-color: #007B9F;
|
||||
border-color: #007B9F;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-link {
|
||||
background-color: #444;
|
||||
color: #FFD600;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.date-link:hover {
|
||||
background-color: #555;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.activitySpreadInhabitant2 {
|
||||
background-color: #007B9F;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.activityVotePost {
|
||||
background-color: #557d3b;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.update-banner {
|
||||
background-color: #1a1400;
|
||||
border-bottom-color: #3a2e00;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.update-banner-link {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.oasis-footer-center a {
|
||||
color: #FFA500;
|
||||
}
|
||||
|
||||
.oasis-footer-center a:hover {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.snh-invite-code {
|
||||
color: #FFA500 !important;
|
||||
}
|
||||
|
||||
/* Blockexplorer */
|
||||
.stats-kpi-label { color: #ffa300 !important; }
|
||||
.stats-kpi-value { color: #FFD700 !important; }
|
||||
.carbon-bar-note, .carbon-bar-formula { color: #FFD700 !important; }
|
||||
.graphos-node-label { fill: #ddd !important; }
|
||||
.graphos-node-label-me { fill: #ffa500 !important; }
|
||||
.graphos-legend { color: #ddd !important; }
|
||||
.blockchain-view { background-color: #191b20 !important; color: #FFD700 !important; }
|
||||
.block { background: #23242a !important; }
|
||||
.block:hover { box-shadow: 0 8px 32px rgba(35,40,50,0.18) !important; }
|
||||
.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #ffa300 !important; }
|
||||
.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #FFD700 !important; }
|
||||
.block-content-preview, .block-content { background: #222326 !important; color: #FFD700 !important; }
|
||||
.block-author { color: #FFD700 !important; background: rgba(255,163,0,0.08) !important; }
|
||||
.block-author:hover { color: #FFDD44 !important; }
|
||||
.block-url { color: #FFD700 !important; }
|
||||
.block-row--details .block-url { background: #1f2023 !important; }
|
||||
.block-row--details .block-url:hover { background: #292b36 !important; color: #ffa300 !important; }
|
||||
.btn-singleview { background: #1e1f23 !important; color: #ffa300 !important; }
|
||||
.btn-singleview:hover { background: #2d2e34 !important; color: #FFD700 !important; }
|
||||
.btn-spread-on, .content-actions .btn-pin-on { background: #ffa300 !important; color: #000000 !important; }
|
||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #ffb733 !important; color: #000000 !important; }
|
||||
.btn-back { background: #21232b !important; color: #ffa300 !important; }
|
||||
.btn-back:hover { background: #292b36 !important; color: #FFD700 !important; }
|
||||
.block-info-table td, .pm-info-table td { border-color: #444 !important; }
|
||||
.block-diagram { border-color: #555 !important; background: #191b20 !important; }
|
||||
.block-diagram-ruler { color: #ffa300 !important; background: #111 !important; border-bottom-color: #555 !important; }
|
||||
.block-diagram-ruler span { color: #ffa300 !important; }
|
||||
.block-diagram-cell { border-color: #555 !important; background: #1e1f23 !important; }
|
||||
.bd-label { color: #ffa300 !important; }
|
||||
.bd-value { color: #FFD700 !important; }
|
||||
|
||||
/* Tribes */
|
||||
.tribe-card { background: #23242a !important; border-color: #444 !important; }
|
||||
.tribe-card:hover { border-color: #ffa300 !important; }
|
||||
.tribe-card-description { color: #cfd3e1 !important; }
|
||||
.tribe-info-table td { border-color: #444 !important; }
|
||||
.tribe-info-label { color: #ffa300 !important; background: #1e1f23 !important; }
|
||||
.tribe-info-value { color: #FFD700 !important; background: #1e1f23 !important; }
|
||||
.tribe-info-empty { color: #9aa3b2 !important; }
|
||||
.tribe-card-members { border-color: #444 !important; background: #1e1f23 !important; }
|
||||
.tribe-members-count { color: #ffa300 !important; }
|
||||
.tribe-card-actions { border-color: #444 !important; background: #1e1f23 !important; }
|
||||
.tribe-action-btn { border-color: #ffa300 !important; color: #ffa300 !important; }
|
||||
.tribe-action-btn:hover { background: #ffa300 !important; color: #000 !important; }
|
||||
.tribe-subtribe-link { background: #191b20 !important; border-color: #444 !important; color: #ffa300 !important; }
|
||||
.tribe-thumb-link { border-color: #444 !important; }
|
||||
.tribe-thumb-link:hover { border-color: #ffa300 !important; }
|
||||
.tribe-subtribe-link:hover { background: #333 !important; }
|
||||
.tribe-parent-image { border-color: #ffa300 !important; }
|
||||
.tribe-parent-box { background: #1e1f23 !important; }
|
||||
382
src/client/assets/themes/Matrix-SNH.css
Normal file
382
src/client/assets/themes/Matrix-SNH.css
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
body {
|
||||
background-color: #000000 !important;
|
||||
color: #00FF00 !important;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
background-color: #000000;
|
||||
color: #00FF00;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 2px solid #00FF00;
|
||||
}
|
||||
|
||||
.sidebar-left, .sidebar-right {
|
||||
background-color: #000000;
|
||||
color: #00FF00;
|
||||
border-right: 2px solid #00FF00;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
background-color: #000000;
|
||||
color: #00FF00;
|
||||
padding: 20px;
|
||||
border-left: 2px solid #00FF00;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button, input[type="submit"], input[type="button"] {
|
||||
background-color: #000000;
|
||||
color: #00FF00;
|
||||
border: 2px solid #00FF00;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button:hover, input[type="submit"]:hover, input[type="button"]:hover {
|
||||
background-color: #00FF00;
|
||||
color: #000000;
|
||||
border-color: #00FF00;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
background-color: #1A1A1A;
|
||||
color: #00FF00;
|
||||
border: 1px solid #00FF00;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00FF00;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: #000000;
|
||||
color: #00FF00;
|
||||
width: 100%;
|
||||
border: 1px solid #00FF00;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #00FF00;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #1A1A1A;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
background-color: #000000;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: #00FF00;
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.profile {
|
||||
background-color: #1A1A1A;
|
||||
color: #00FF00;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #00FF00;
|
||||
box-shadow: 0 2px 5px rgba(0,255,0,0.5);
|
||||
}
|
||||
|
||||
.profile .name {
|
||||
color: #00FF00;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 4px solid #00FF00;
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
article, section {
|
||||
background-color: #1A1A1A;
|
||||
color: #00FF00;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #00FF00;
|
||||
box-shadow: 0 4px 10px rgba(0, 255, 0, 0.5);
|
||||
}
|
||||
|
||||
.post-preview {
|
||||
background-color: #00FF00;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.post-preview img {
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #00FF00;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
background-color: #1A1A1A;
|
||||
border-color: #00FF00;
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
footer a {
|
||||
background-color: #00FF00;
|
||||
color: #000000;
|
||||
padding: 8px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-bar-left,
|
||||
.top-bar-mid,
|
||||
.top-bar-right {
|
||||
background-color: #000000 !important;
|
||||
border: 2px solid #00FF00 !important;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 0 12px #00FF00;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sidebar-left,
|
||||
.sidebar-right {
|
||||
background-color: #000000 !important;
|
||||
border: 2px solid #00FF00 !important;
|
||||
box-shadow: 0 0 15px #00FF00;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul,
|
||||
.sidebar-right nav ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li,
|
||||
.sidebar-right nav ul li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li a,
|
||||
.sidebar-right nav ul li a,
|
||||
.header nav ul li a {
|
||||
background-color: #000000 !important;
|
||||
color: #00FF00 !important;
|
||||
border: 1px solid #00FF00 !important;
|
||||
font-weight: bold;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
box-shadow: 0 0 6px #00FF00;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li a:hover,
|
||||
.sidebar-right nav ul li a:hover,
|
||||
.header nav ul li a:hover {
|
||||
background-color: #00FF00 !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
color: #00FF00;
|
||||
font-family: 'Courier New', monospace;
|
||||
background-color: #1A1A1A;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link {
|
||||
color: #00FF00;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link:hover {
|
||||
background: #00FF00;
|
||||
color: #000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link {
|
||||
background-color: #00FF00;
|
||||
color: #000000;
|
||||
border-color: #00FF00;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
a.user-link:hover {
|
||||
background-color: #000000;
|
||||
border-color: #00FF00;
|
||||
color: #00FF00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link:focus {
|
||||
background-color: #00FF00;
|
||||
border-color: #00FF00;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.date-link {
|
||||
background-color: #00FF00;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.date-link:hover {
|
||||
background-color: #000000;
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.activitySpreadInhabitant2 {
|
||||
background-color: #1A1A1A;
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.activityVotePost {
|
||||
background-color: #00FF00;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.update-banner {
|
||||
background-color: #001a00;
|
||||
border-bottom-color: #003300;
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.update-banner-link {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.snh-invite-code {
|
||||
color: #00FF00 !important;
|
||||
}
|
||||
|
||||
.stats-kpi-label { color: #00FF00 !important; }
|
||||
.stats-kpi-value { color: #00FF00 !important; }
|
||||
.carbon-bar-note, .carbon-bar-formula { color: #00FF00 !important; }
|
||||
.graphos-node-label { fill: #00FF00 !important; }
|
||||
.graphos-node-label-me { fill: #ffa500 !important; font-weight: bold !important; }
|
||||
.graphos-legend { color: #00FF00 !important; }
|
||||
.graphos-edge { stroke: #003300 !important; }
|
||||
/* Blockexplorer */
|
||||
.blockchain-view { background-color: #000000 !important; color: #00FF00 !important; }
|
||||
.block { background: #1A1A1A !important; border: 1px solid #00FF00 !important; }
|
||||
.block:hover { box-shadow: 0 0 15px rgba(0,255,0,0.3) !important; }
|
||||
.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #00FF00 !important; }
|
||||
.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #00FF00 !important; }
|
||||
.block-content-preview, .block-content { background: #1A1A1A !important; color: #00FF00 !important; }
|
||||
.block-author { color: #00FF00 !important; background: rgba(0,255,0,0.08) !important; }
|
||||
.block-author:hover { color: #00FF00 !important; }
|
||||
.block-url { color: #00FF00 !important; }
|
||||
.block-row--details .block-url { background: #1A1A1A !important; }
|
||||
.block-row--details .block-url:hover { background: #00FF00 !important; color: #000000 !important; }
|
||||
.btn-singleview { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; }
|
||||
.btn-singleview:hover { background: #00FF00 !important; color: #000000 !important; }
|
||||
.btn-spread-on, .content-actions .btn-pin-on { background: #00FF00 !important; color: #000000 !important; }
|
||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #66FF66 !important; color: #000000 !important; }
|
||||
.btn-back { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; }
|
||||
.btn-back:hover { background: #00FF00 !important; color: #000000 !important; }
|
||||
.block-info-table td, .pm-info-table td { border-color: #00FF00 !important; }
|
||||
.block-diagram { border-color: #00FF00 !important; background: #000000 !important; }
|
||||
.block-diagram-ruler { color: #00FF00 !important; background: #000000 !important; border-bottom-color: #00FF00 !important; }
|
||||
.block-diagram-ruler span { color: #00FF00 !important; }
|
||||
.block-diagram-cell { border-color: #00FF00 !important; background: #1A1A1A !important; }
|
||||
.bd-label { color: #00FF00 !important; }
|
||||
.bd-value { color: #00FF00 !important; }
|
||||
.deleted-label { color: #ff3333 !important; }
|
||||
|
||||
/* Tribes */
|
||||
.tribe-card { background: #1A1A1A !important; border-color: #00FF00 !important; }
|
||||
.tribe-card:hover { box-shadow: 0 0 15px rgba(0,255,0,0.3) !important; }
|
||||
.tribe-card-title { color: #00FF00 !important; }
|
||||
.tribe-card-description { color: #00FF00 !important; }
|
||||
.tribe-info-table td { border-color: #00FF00 !important; }
|
||||
.tribe-info-label { color: #00FF00 !important; background: #1A1A1A !important; }
|
||||
.tribe-info-value { color: #00FF00 !important; background: #1A1A1A !important; }
|
||||
.tribe-info-empty { color: #006600 !important; }
|
||||
.tribe-card-members { border-color: #00FF00 !important; background: #1A1A1A !important; }
|
||||
.tribe-members-count { color: #00FF00 !important; }
|
||||
.tribe-card-actions { border-color: #00FF00 !important; background: #1A1A1A !important; }
|
||||
.tribe-action-btn { border-color: #00FF00 !important; color: #00FF00 !important; }
|
||||
.tribe-action-btn:hover { background: #00FF00 !important; color: #000 !important; }
|
||||
.tribe-subtribe-link { background: #000000 !important; border-color: #00FF00 !important; color: #00FF00 !important; }
|
||||
.tribe-thumb-link { border-color: #00FF00 !important; }
|
||||
.tribe-thumb-link:hover { box-shadow: 0 0 10px rgba(0,255,0,0.3) !important; }
|
||||
.tribe-subtribe-link:hover { background: #00FF00 !important; color: #000 !important; }
|
||||
.tribe-parent-image { border-color: #00FF00 !important; }
|
||||
.tribe-parent-box { background: #1A1A1A !important; }
|
||||
|
||||
.oasis-nav-header { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; }
|
||||
.oasis-nav-header:hover { color: #000000 !important; background: #00FF00 !important; }
|
||||
.oasis-nav-header .emoji, .oasis-nav-header .nav-text, .oasis-nav-arrow { color: inherit !important; }
|
||||
.oasis-nav-list li a { color: #00FF00 !important; }
|
||||
.oasis-nav-list li a:hover { color: #00FF00 !important; opacity: 1 !important; }
|
||||
.oasis-nav-list .emoji, .oasis-nav-list .nav-text { color: inherit !important; }
|
||||
nav, .sidebar-left nav, .sidebar-right nav, .sidebar-left ul, .sidebar-right ul { color: #00FF00 !important; }
|
||||
.sidebar-left a, .sidebar-right a { color: #00FF00 !important; }
|
||||
|
||||
.ai-ask-form .ai-ask-input, .ai-ask-form .ai-ask-btn { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; }
|
||||
.ai-ask-form .ai-ask-input::placeholder { color: rgba(0,255,0,0.55) !important; }
|
||||
.ai-ask-form .ai-ask-input:focus { border-color: #00FF00 !important; background: #1A1A1A !important; box-shadow: 0 0 6px rgba(0,255,0,0.4) !important; }
|
||||
.ai-ask-form .ai-ask-btn:hover { background: #00FF00 !important; color: #000000 !important; border-color: #00FF00 !important; }
|
||||
.ai-nav-results, .ai-nav-result-card { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; }
|
||||
.ai-nav-result-card .card-label { color: #00FF00 !important; }
|
||||
.ai-nav-result-card a { color: #00FF00 !important; }
|
||||
.ai-nav-result-card a:hover { background: #00FF00 !important; color: #000000 !important; }
|
||||
.ai-nav-query { color: #00FF00 !important; }
|
||||
.trending-card.own-content { box-shadow: inset 3px 0 0 #00FF00; }
|
||||
398
src/client/assets/themes/Purple-SNH.css
Normal file
398
src/client/assets/themes/Purple-SNH.css
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
body {
|
||||
background-color: #4B0A6D;
|
||||
color: #E5E5E5;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 2px solid #9B1C96;
|
||||
}
|
||||
|
||||
.sidebar-left, .sidebar-right {
|
||||
background-color: #39006D;
|
||||
border: 1px solid #9B1C96;
|
||||
color: #E5E5E5;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.main-column {
|
||||
background-color: #1A1A1A;
|
||||
border: 1px solid #9B1C96;
|
||||
padding: 20px;
|
||||
color: #E5E5E5;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button, input[type="submit"], input[type="button"] {
|
||||
background-color: #9B1C96;
|
||||
color: #E5E5E5;
|
||||
border: 2px solid #6A0066;
|
||||
border-radius: 10px;
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover, input[type="submit"]:hover, input[type="button"]:hover {
|
||||
background-color: #6A0066;
|
||||
border-color: #9B1C96;
|
||||
box-shadow: 0 0 15px rgba(155, 28, 150, 0.8);
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
background-color: #333333;
|
||||
color: #E5E5E5;
|
||||
border: 1px solid #9B1C96;
|
||||
border-radius: 5px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
background-color: #39006D;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #9B1C96;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #E5E5E5;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: #1A1A1A;
|
||||
color: #E5E5E5;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #6A0066;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
background-color: #39006D;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: #E5E5E5;
|
||||
padding: 15px;
|
||||
display: inline-block;
|
||||
text-transform: none;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.profile {
|
||||
background-color: #333333;
|
||||
color: #E5E5E5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.profile .name {
|
||||
color: #9B1C96;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 4px solid #9B1C96;
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
article, section {
|
||||
background-color: #333333;
|
||||
color: #E5E5E5;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.post-preview {
|
||||
background-color: #39006D;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.post-preview img {
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #9B1C96;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #1A1A1A;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
background-color: #333333;
|
||||
border-color: #9B1C96;
|
||||
color: #E5E5E5;
|
||||
}
|
||||
|
||||
footer a {
|
||||
background-color: #9B1C96;
|
||||
color: #FFFFFF;
|
||||
padding: 8px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
background-color: #6A0066;
|
||||
}
|
||||
|
||||
|
||||
.sidebar-left nav ul li a,
|
||||
.sidebar-right nav ul li a,
|
||||
.header nav ul li a {
|
||||
background-color: #682B94 !important;
|
||||
color: #FFE082 !important;
|
||||
border: 1px solid #B86ADE !important;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li a:hover,
|
||||
.sidebar-right nav ul li a:hover,
|
||||
.header nav ul li a:hover {
|
||||
background-color: #682B94 !important;
|
||||
border-color: #FFD54F !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #2D0B47 !important;
|
||||
}
|
||||
|
||||
.main-column,
|
||||
article,
|
||||
section,
|
||||
.action-container,
|
||||
.profile,
|
||||
.post-preview {
|
||||
background-color: #3C1360 !important;
|
||||
border-color: #B86ADE !important;
|
||||
color: #FFEEDB !important;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
background-color: #4B1A72 !important;
|
||||
color: #FFFFFF !important;
|
||||
border-color: #BB5EFF !important;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="button"] {
|
||||
background-color: #A34AD8 !important;
|
||||
color: #FFFFFF !important;
|
||||
border-color: #751E9F !important;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #5A1A85 !important;
|
||||
border-bottom: 1px solid #B86ADE !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #5A1A85 !important;
|
||||
}
|
||||
|
||||
.top-bar-left,
|
||||
.top-bar-mid,
|
||||
.top-bar-right {
|
||||
background-color: #39006D !important;
|
||||
border: 1px solid #9B1C96 !important;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sidebar-left,
|
||||
.sidebar-right {
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul,
|
||||
.sidebar-right nav ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-left nav ul li,
|
||||
.sidebar-right nav ul li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
color: #E5E5E5;
|
||||
box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.2);
|
||||
background-color: #3C1360;
|
||||
border-color: #B86ADE;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #9B1C96;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
color: #B86ADE;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link {
|
||||
color: #FFFFFF;
|
||||
background: #D94F4F;
|
||||
}
|
||||
|
||||
.card-tags a.tag-link:hover {
|
||||
background: #B86ADE;
|
||||
color: #E5E5E5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link {
|
||||
background-color: #FFD600;
|
||||
color: #3C1360;
|
||||
border-color: #FFD600;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
a.user-link:hover {
|
||||
background-color: #FFB600;
|
||||
border-color: #FFB600;
|
||||
color: #3C1360;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-link:focus {
|
||||
background-color: #9A2F2F;
|
||||
border-color: #9A2F2F;
|
||||
color: #E5E5E5;
|
||||
}
|
||||
|
||||
.date-link {
|
||||
background-color: #2F3C32;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-link:hover {
|
||||
background-color: #3E4A3D;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.activitySpreadInhabitant2 {
|
||||
background-color: #3E4A3D;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.activityVotePost {
|
||||
background-color: #3B5C42;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.update-banner {
|
||||
background-color: #1a0025;
|
||||
border-bottom-color: #3d004d;
|
||||
color: #E5E5E5;
|
||||
}
|
||||
|
||||
.update-banner-link {
|
||||
color: #c44bc1;
|
||||
}
|
||||
|
||||
.snh-invite-code {
|
||||
color: #9B1C96 !important;
|
||||
}
|
||||
|
||||
.stats-kpi-label { color: #B86ADE !important; }
|
||||
.stats-kpi-value { color: #FFEEDB !important; }
|
||||
.carbon-bar-note, .carbon-bar-formula { color: #FFEEDB !important; }
|
||||
.graphos-node-label { fill: #FFEEDB !important; }
|
||||
.graphos-node-label-me { fill: #ffa500 !important; }
|
||||
.graphos-legend { color: #FFEEDB !important; }
|
||||
/* Blockexplorer */
|
||||
.blockchain-view { background-color: #2D0B47 !important; color: #FFEEDB !important; }
|
||||
.block { background: #3C1360 !important; border: 1px solid #B86ADE !important; }
|
||||
.block:hover { box-shadow: 0 0 15px rgba(184,106,222,0.3) !important; }
|
||||
.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #B86ADE !important; }
|
||||
.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #FFEEDB !important; }
|
||||
.block-content-preview, .block-content { background: #4B1A72 !important; color: #FFEEDB !important; }
|
||||
.block-author { color: #FFD600 !important; background: rgba(255,214,0,0.08) !important; }
|
||||
.block-author:hover { color: #FFB600 !important; }
|
||||
.block-url { color: #B86ADE !important; }
|
||||
.block-row--details .block-url { background: #4B1A72 !important; }
|
||||
.block-row--details .block-url:hover { background: #5A1A85 !important; color: #FFD600 !important; }
|
||||
.btn-singleview { background: #4B1A72 !important; color: #B86ADE !important; }
|
||||
.btn-singleview:hover { background: #5A1A85 !important; color: #FFD600 !important; }
|
||||
.btn-spread-on, .content-actions .btn-pin-on { background: #B86ADE !important; color: #2A0E42 !important; }
|
||||
.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #CE8CE9 !important; color: #2A0E42 !important; }
|
||||
.btn-back { background: #A34AD8 !important; color: #FFFFFF !important; }
|
||||
.btn-back:hover { background: #751E9F !important; color: #FFFFFF !important; }
|
||||
.block-info-table td, .pm-info-table td { border-color: #B86ADE !important; }
|
||||
.block-diagram { border-color: #B86ADE !important; background: #2D0B47 !important; }
|
||||
.block-diagram-ruler { color: #B86ADE !important; background: #1A0030 !important; border-bottom-color: #B86ADE !important; }
|
||||
.block-diagram-ruler span { color: #B86ADE !important; }
|
||||
.block-diagram-cell { border-color: #B86ADE !important; background: #3C1360 !important; }
|
||||
.bd-label { color: #B86ADE !important; }
|
||||
.bd-value { color: #FFEEDB !important; }
|
||||
.deleted-label { color: #ff5555 !important; }
|
||||
|
||||
/* Tribes */
|
||||
.tribe-card { background: #3C1360 !important; border-color: #B86ADE !important; }
|
||||
.tribe-card:hover { box-shadow: 0 0 15px rgba(184,106,222,0.3) !important; }
|
||||
.tribe-card-title { color: #FFEEDB !important; }
|
||||
.tribe-card-description { color: #FFEEDB !important; }
|
||||
.tribe-info-table td { border-color: #B86ADE !important; }
|
||||
.tribe-info-label { color: #B86ADE !important; background: #3C1360 !important; }
|
||||
.tribe-info-value { color: #FFEEDB !important; background: #3C1360 !important; }
|
||||
.tribe-info-empty { color: #8844aa !important; }
|
||||
.tribe-card-members { border-color: #B86ADE !important; background: #2D0B47 !important; }
|
||||
.tribe-members-count { color: #FFD600 !important; }
|
||||
.tribe-card-actions { border-color: #B86ADE !important; background: #2D0B47 !important; }
|
||||
.tribe-action-btn { border-color: #B86ADE !important; color: #B86ADE !important; }
|
||||
.tribe-action-btn:hover { background: #B86ADE !important; color: #000 !important; }
|
||||
.tribe-subtribe-link { background: #4B1A72 !important; border-color: #B86ADE !important; color: #FFD600 !important; }
|
||||
.tribe-thumb-link { border-color: #B86ADE !important; }
|
||||
.tribe-thumb-link:hover { border-color: #FFD600 !important; }
|
||||
.tribe-subtribe-link:hover { background: #5A1A85 !important; }
|
||||
.tribe-parent-image { border-color: #B86ADE !important; }
|
||||
.tribe-parent-box { background: #3C1360 !important; }
|
||||
.trending-card.own-content { box-shadow: inset 3px 0 0 #B86ADE; }
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
module.exports = {
|
||||
en: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -54,6 +55,7 @@ module.exports = {
|
|||
spreadMore: "more"
|
||||
},
|
||||
es: {
|
||||
chatReply: "Responder",
|
||||
bbAdd: "Añadir",
|
||||
bbDone: "Hecho",
|
||||
bbFull: "Máximo alcanzado",
|
||||
|
|
@ -102,6 +104,7 @@ module.exports = {
|
|||
totalspreads: "Replicas totales"
|
||||
},
|
||||
fr: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -145,6 +148,7 @@ module.exports = {
|
|||
spreadMore: "plus"
|
||||
},
|
||||
eu: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -188,6 +192,7 @@ module.exports = {
|
|||
spreadMore: "gehiago"
|
||||
},
|
||||
de: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -231,6 +236,7 @@ module.exports = {
|
|||
spreadMore: "mehr"
|
||||
},
|
||||
it: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -274,6 +280,7 @@ module.exports = {
|
|||
spreadMore: "altro"
|
||||
},
|
||||
pt: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -317,6 +324,7 @@ module.exports = {
|
|||
spreadMore: "mais"
|
||||
},
|
||||
zh: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -360,6 +368,7 @@ module.exports = {
|
|||
spreadMore: "更多"
|
||||
},
|
||||
ar: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -403,6 +412,7 @@ module.exports = {
|
|||
spreadMore: "المزيد"
|
||||
},
|
||||
hi: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
@ -446,6 +456,7 @@ module.exports = {
|
|||
spreadMore: "और"
|
||||
},
|
||||
ru: {
|
||||
chatReply: "Reply",
|
||||
bbAdd: "Add",
|
||||
bbDone: "Done",
|
||||
bbFull: "Maximum reached",
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ module.exports = ({ host, port, middleware, allowHost }) => {
|
|||
app.use(koaStatic(path.join(__dirname, 'public')));
|
||||
|
||||
app.use(async (ctx, next) => {
|
||||
|
||||
//console.log("Requesting:", ctx.path); // uncomment to check for HTTP requests
|
||||
|
||||
if (httpDebug) console.log(`[http] ${ctx.method} ${ctx.path}`);
|
||||
|
||||
const isClearnet = isClearnetPath(ctx.request);
|
||||
const csp = isClearnet
|
||||
|
|
|
|||
76
src/client/public/js/pdf-viewer.js
Normal file
76
src/client/public/js/pdf-viewer.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof pdfjsLib === 'undefined') return;
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.mjs';
|
||||
|
||||
document.querySelectorAll('.pdf-viewer-container').forEach(async container => {
|
||||
const pdfUrl = container.getAttribute('data-pdf-url');
|
||||
if (!pdfUrl) return;
|
||||
|
||||
const pdf = await pdfjsLib.getDocument(pdfUrl).promise;
|
||||
let currentPage = 1;
|
||||
let scale = 1.5;
|
||||
let rotation = 0;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
container.innerHTML = '';
|
||||
container.appendChild(canvas);
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'pdf-controls';
|
||||
controls.innerHTML = `
|
||||
<button id="prev">⬅️</button>
|
||||
<button id="next">➡️</button>
|
||||
<button id="zoomIn">🔍+</button>
|
||||
<button id="zoomOut">🔍−</button>
|
||||
<button id="rotate">↻</button>
|
||||
<button id="download">⬇️</button>
|
||||
<button id="fullscreen">🔲</button>
|
||||
<button id="metadata">ℹ️</button>
|
||||
`;
|
||||
|
||||
container.appendChild(controls);
|
||||
|
||||
const renderPage = async (num) => {
|
||||
const page = await pdf.getPage(num);
|
||||
const viewport = page.getViewport({ scale, rotation });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
};
|
||||
|
||||
const goToPage = (delta) => {
|
||||
const newPage = currentPage + delta;
|
||||
if (newPage >= 1 && newPage <= pdf.numPages) {
|
||||
currentPage = newPage;
|
||||
renderPage(currentPage);
|
||||
}
|
||||
};
|
||||
|
||||
renderPage(currentPage);
|
||||
|
||||
controls.querySelector('#prev').onclick = () => goToPage(-1);
|
||||
controls.querySelector('#next').onclick = () => goToPage(1);
|
||||
controls.querySelector('#zoomIn').onclick = () => { scale += 0.2; renderPage(currentPage); };
|
||||
controls.querySelector('#zoomOut').onclick = () => { scale = Math.max(0.5, scale - 0.2); renderPage(currentPage); };
|
||||
controls.querySelector('#rotate').onclick = () => { rotation = (rotation + 90) % 360; renderPage(currentPage); };
|
||||
controls.querySelector('#download').onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = pdfUrl;
|
||||
a.download = 'document.pdf';
|
||||
a.click();
|
||||
};
|
||||
controls.querySelector('#fullscreen').onclick = () => {
|
||||
if (canvas.requestFullscreen) canvas.requestFullscreen();
|
||||
else if (canvas.webkitRequestFullscreen) canvas.webkitRequestFullscreen();
|
||||
else if (canvas.mozRequestFullScreen) canvas.mozRequestFullScreen();
|
||||
else if (canvas.msRequestFullscreen) canvas.msRequestFullscreen();
|
||||
};
|
||||
controls.querySelector('#metadata').onclick = async () => {
|
||||
const info = await pdf.getMetadata();
|
||||
alert(`Title: ${info.info.Title || 'N/A'}\nAuthor: ${info.info.Author || 'N/A'}\nPDF Producer: ${info.info.Producer || 'N/A'}`);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
22
src/client/public/js/pdf.min.mjs
Normal file
22
src/client/public/js/pdf.min.mjs
Normal file
File diff suppressed because one or more lines are too long
22
src/client/public/js/pdf.worker.min.mjs
Normal file
22
src/client/public/js/pdf.worker.min.mjs
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -738,6 +738,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }
|
|||
if (a.type === 'task' && String(c.isPublic || '').toUpperCase() === 'PRIVATE' && a.author !== userId && !(Array.isArray(c.assignees) && c.assignees.includes(userId))) return false;
|
||||
if (a.type === 'forum' && c.isPrivate === true && a.author !== userId) return false;
|
||||
if (a.type === 'job' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId && !(Array.isArray(c.subscribers) && c.subscribers.includes(userId))) return false;
|
||||
if (a.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false;
|
||||
if (a.type === 'market' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.seller !== userId) return false;
|
||||
if (a.type === 'shop' && String(c.visibility || '').toUpperCase() === 'CLOSED' && a.author !== userId) return false;
|
||||
if (a.type === 'curriculum' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false;
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ module.exports = ({ cooler }) => {
|
|||
const isOwner = viewer === target;
|
||||
const arr = (v) => Array.isArray(v) ? v : [];
|
||||
const up = (v) => String(v || '').toUpperCase();
|
||||
const COUNTED = new Set(['post','event','task','forum','tribe','market','job','project','industry','shop','image','video','audio','document','bookmark','transfer','map']);
|
||||
const COUNTED = new Set(['post','event','task','forum','tribe','market','job','housing','project','industry','shop','image','video','audio','document','bookmark','transfer','map']);
|
||||
const accessible = (type, c) => {
|
||||
if (c.encryptedPayload) return false;
|
||||
switch (type) {
|
||||
|
|
@ -377,6 +377,7 @@ module.exports = ({ cooler }) => {
|
|||
case 'event': return String(c.isPublic || '').toLowerCase() !== 'private' || isOwner || arr(c.attendees).includes(viewer);
|
||||
case 'forum': return c.isPrivate !== true || isOwner;
|
||||
case 'job': return up(c.visibility) !== 'HIDDEN' || isOwner || arr(c.subscribers).includes(viewer);
|
||||
case 'housing': return up(c.visibility) !== 'HIDDEN' || isOwner;
|
||||
case 'market': return up(c.visibility) !== 'HIDDEN' || isOwner;
|
||||
case 'shop': return up(c.visibility) !== 'CLOSED' || isOwner;
|
||||
case 'tribe': { const st = up(c.status); return !(st === 'PRIVATE' || st === 'INVITE-ONLY') || isOwner || arr(c.members).includes(viewer); }
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
|||
const jobId = c.jobId
|
||||
const k = `${jobId}::${author}`
|
||||
const prev = jobSubLatest.get(k)
|
||||
if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
|
||||
if (!prev || ts >= prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
|
|||
const jobId = c.jobId
|
||||
const k = `${jobId}::${author}`
|
||||
const prev = jobSubLatest.get(k)
|
||||
if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
|
||||
if (!prev || ts >= prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,11 @@ function getGoverningHouseKey(now = new Date()) {
|
|||
return HOUSE_KEYS[now.getMonth() % HOUSE_KEYS.length];
|
||||
}
|
||||
|
||||
function getGoverningPeriodId(now = new Date()) {
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`;
|
||||
}
|
||||
|
||||
module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||
let ssb;
|
||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||
|
|
@ -922,6 +927,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
PROFILE_QUESTIONS,
|
||||
computeCycle,
|
||||
getGoverningHouseKey,
|
||||
getGoverningPeriodId,
|
||||
publishJoin,
|
||||
publishLeaveLarp,
|
||||
getUserHouse,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ module.exports = ({ cooler }) => {
|
|||
const validTypes = [
|
||||
'bookmark', 'votes', 'transfer',
|
||||
'feed', 'image', 'audio', 'video', 'document', 'torrent',
|
||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct'
|
||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market'
|
||||
];
|
||||
|
||||
const getPreview = c => {
|
||||
|
|
@ -46,7 +46,9 @@ module.exports = ({ cooler }) => {
|
|||
report: 'reportOpinion',
|
||||
task: 'taskOpinion',
|
||||
event: 'eventOpinion',
|
||||
shopProduct: 'shopOpinion'
|
||||
shopProduct: 'shopOpinion',
|
||||
housing: 'housingOpinion',
|
||||
market: 'marketOpinion'
|
||||
};
|
||||
|
||||
const createVote = async (contentId, category) => {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ module.exports = ({ cooler }) => {
|
|||
async sentRefs() {
|
||||
const out = new Set();
|
||||
let messages = [];
|
||||
try { messages = await this.listAllPrivate(); } catch (_) { return out; }
|
||||
try { messages = await this.listAllPrivate({ includeDeleted: true }); } catch (_) { return out; }
|
||||
for (const m of (messages || [])) {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!c || !c.ref) continue;
|
||||
|
|
@ -116,7 +116,8 @@ module.exports = ({ cooler }) => {
|
|||
return publishAsync(tombstone, tombstoneRecps);
|
||||
},
|
||||
|
||||
async listAllPrivate() {
|
||||
async listAllPrivate(opts = {}) {
|
||||
const includeDeleted = !!(opts && opts.includeDeleted);
|
||||
const ssbClient = await openSsb();
|
||||
const raw = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
|
|
@ -174,7 +175,7 @@ module.exports = ({ cooler }) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
return posts.filter(m => m && m.key && !tombed.has(m.key));
|
||||
return posts.filter(m => m && m.key && (includeDeleted || !tombed.has(m.key)));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ module.exports = ({ cooler }) => {
|
|||
if (goal < 0) goal = 0
|
||||
|
||||
const deadlineISO = data.deadline ? new Date(data.deadline).toISOString() : null
|
||||
if (deadlineISO && new Date(deadlineISO).getTime() < Date.now()) throw new Error("The deadline cannot be in the past")
|
||||
|
||||
const content = {
|
||||
type: TYPE,
|
||||
|
|
@ -323,7 +324,11 @@ module.exports = ({ cooler }) => {
|
|||
}
|
||||
|
||||
let deadline = patch.deadline === undefined ? current.deadline : patch.deadline
|
||||
if (deadline != null && deadline !== "") deadline = new Date(deadline).toISOString()
|
||||
if (deadline != null && deadline !== "") {
|
||||
deadline = new Date(deadline).toISOString()
|
||||
const changed = !current.deadline || new Date(current.deadline).getTime() !== new Date(deadline).getTime()
|
||||
if (changed && new Date(deadline).getTime() < Date.now()) throw new Error("The deadline cannot be in the past")
|
||||
}
|
||||
else if (deadline === "") deadline = null
|
||||
|
||||
const updated = {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => {
|
|||
'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed',
|
||||
'votes', 'report', 'task', 'event', 'bookmark', 'document',
|
||||
'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim',
|
||||
'project', 'job', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'poll'
|
||||
'project', 'job', 'housing', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'poll'
|
||||
];
|
||||
|
||||
const getRelevantFields = (type, content) => {
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ module.exports = ({ cooler, padsModel, tribesModel }) => {
|
|||
if (c.type === 'task' && String(c.isPublic).toUpperCase() === 'PRIVATE') return false;
|
||||
if ((c.type === 'chat' || c.type === 'pad' || c.type === 'map' || c.type === 'calendar') && c.status === 'INVITE-ONLY' && c.author !== viewerId && !(Array.isArray(c.members) && c.members.includes(viewerId))) return false;
|
||||
if (c.type === 'shop' && c.visibility === 'CLOSED') return false;
|
||||
if (c.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.author !== viewerId) return false;
|
||||
if (c.type === 'poll' && c.chatId) return false;
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ module.exports = ({ cooler }) => {
|
|||
const types = [
|
||||
'bookmark', 'votes', 'poll', 'feed',
|
||||
'image', 'audio', 'video', 'document', 'torrent', 'transfer',
|
||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct'
|
||||
'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market'
|
||||
];
|
||||
|
||||
const categories = opinionCategories;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const FILTER_LABELS = {
|
|||
const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs'];
|
||||
const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts'];
|
||||
const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
|
||||
const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore'];
|
||||
const CAT_BLOCK3 = ['banking', 'job', 'housing', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore'];
|
||||
const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent'];
|
||||
|
||||
const SEARCH_FIELDS = ['author','id','from','to'];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views")
|
||||
const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li, details, summary } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, userLinkLabel, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions } = require("./main_views")
|
||||
const { renderEncryptedChip } = require("./clearnet_view")
|
||||
const { renderResults, renderBallot, outcomeOf } = require("./polls_view")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
const { renderZoomableImage } = require("./gallery_view")
|
||||
|
||||
const userId = config.keys.id
|
||||
const safeArr = (v) => (Array.isArray(v) ? v : [])
|
||||
|
|
@ -17,6 +19,13 @@ const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3]
|
|||
const catKey = (c) => "forumCat" + String(c || "").replace(/\./g, "").replace(/[\s-]/g, "").toUpperCase()
|
||||
const catLabel = (c) => i18n[catKey(c)] || c
|
||||
|
||||
const blobSrcOf = (value) => {
|
||||
const s = String(value || "").trim()
|
||||
if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}`
|
||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||
return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null
|
||||
}
|
||||
|
||||
const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => {
|
||||
if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||
const s = String(value).trim()
|
||||
|
|
@ -65,8 +74,19 @@ const renderChatCard = (chat, filter, params = {}) => {
|
|||
renderEncryptedChip(i18n),
|
||||
renderLifespanChip(chat.lifetime, i18n)
|
||||
].filter(Boolean)
|
||||
const href = `/chats/${encodeURIComponent(chat.key)}`
|
||||
|
||||
return div({ class: "tribe-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(chat.key, href, {
|
||||
spread: params.spread || null,
|
||||
author: chat.author,
|
||||
favKind: 'chats',
|
||||
isFavorite: chat.isFavorite,
|
||||
returnTo: buildReturnTo(filter, params),
|
||||
reportTitle: chat.title
|
||||
})
|
||||
),
|
||||
div({ class: "tribe-card-image-wrapper" },
|
||||
a({ href: `/chats/${encodeURIComponent(chat.key)}` },
|
||||
renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-card-hero-image" })
|
||||
|
|
@ -82,13 +102,6 @@ const renderChatCard = (chat, filter, params = {}) => {
|
|||
chat.description ? p({ class: "tribe-card-description" }, chat.description) : null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`)
|
||||
),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(chat.key, (params && params.spreadMap && params.spreadMap.get(chat.key)) || (params && params.spreads));
|
||||
return btn ? div({ class: "card-spread-left" }, btn) : null;
|
||||
})(),
|
||||
div({ class: "visit-btn-centered" },
|
||||
a({ href: `/chats/${encodeURIComponent(chat.key)}`, class: "filter-btn" }, i18n.chatVisitChat)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -104,16 +117,15 @@ const renderChatForm = (filter, chat = {}, params = {}) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
|
||||
span(i18n.title || "Title"), br(),
|
||||
input({ type: "text", name: "title", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(),
|
||||
span(i18n.chatDescription), br(),
|
||||
textarea({ name: "description", rows: 4, placeholder: i18n.chatDescriptionPlaceholder }, chat.description || ""), br(), br(),
|
||||
span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(),
|
||||
span(i18n.uploadMedia), br(),
|
||||
input({ type: "file", name: "image", accept: "image/*" }), br(), br(),
|
||||
span(i18n.chatCategory), br(),
|
||||
select({ name: "category" },
|
||||
option({ value: "" }, "\u2014"),
|
||||
ALL_CATS.map(cat =>
|
||||
option({ value: cat, ...(chat.category === cat ? { selected: true } : {}) }, catLabel(cat))
|
||||
option({ value: cat, ...((chat.category || "GENERAL") === cat ? { selected: true } : {}) }, catLabel(cat))
|
||||
)
|
||||
), br(), br(),
|
||||
span(i18n.chatStatusLabel || "Status"), br(),
|
||||
|
|
@ -140,37 +152,99 @@ const renderMessageText = (text) => {
|
|||
return span({ class: "chat-message-text" }, ...nodes)
|
||||
}
|
||||
|
||||
const renderMessage = (msg, chatAuthor, chatKey, msgById) => {
|
||||
const chatActivityTs = (c) => Math.max(
|
||||
Number(c.lastMsgAt || 0),
|
||||
Date.parse(c.updatedAt || "") || 0,
|
||||
Date.parse(c.createdAt || "") || 0
|
||||
)
|
||||
|
||||
const renderChatTopics = (chats, activeKey) =>
|
||||
div({ class: "chat-topics" },
|
||||
div({ class: "chat-topics-head" },
|
||||
a({ href: "/chats", class: "chat-topics-title-link" }, i18n.chatsTitle),
|
||||
a({ href: "/chats?filter=create", class: "filter-btn chat-topics-new" }, "+")
|
||||
),
|
||||
...safeArr(chats).slice().sort((a, b) => chatActivityTs(b) - chatActivityTs(a)).map(c => {
|
||||
const key = c.rootId || c.key
|
||||
const active = activeKey && String(key) === String(activeKey)
|
||||
return a({ href: `/chats/${encodeURIComponent(key)}`, class: active ? "chat-topic chat-topic-active" : "chat-topic" },
|
||||
span({ class: "chat-topic-title" }, c.title || i18n.chatUntitled),
|
||||
span({ class: "chat-topic-meta" }, `${i18n.chatParticipants}: ${safeArr(c.members).length}`)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const renderSenderLink = (author, isOwner) =>
|
||||
author
|
||||
? a({ href: `/author/${encodeURIComponent(author)}`, class: isOwner ? "chat-bubble-sender chat-bubble-sender-owner" : "chat-bubble-sender" }, userLinkLabel(author))
|
||||
: null
|
||||
|
||||
const renderChatPoll = (poll, chat) => {
|
||||
const isSelf = String(poll.author) === String(userId)
|
||||
const isOwner = String(poll.author) === String(chat.author)
|
||||
const showResults = poll.hasVoted || poll.status === "CLOSED"
|
||||
return div({ class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" },
|
||||
div({ class: isSelf ? "chat-message chat-message-self chat-poll" : "chat-message chat-poll" },
|
||||
isSelf ? null : renderSenderLink(poll.author, isOwner),
|
||||
div({ class: "chat-poll-head" },
|
||||
span({ class: "chat-poll-tag" }, i18n.pollInChat),
|
||||
poll.anonymous ? span({ class: "chat-poll-tag" }, i18n.pollAnonymous) : null,
|
||||
poll.multiple ? span({ class: "chat-poll-tag" }, i18n.pollMultiple) : null,
|
||||
poll.status === "CLOSED" ? span({ class: "chat-poll-tag" }, i18n.pollStatusClosed) : null
|
||||
),
|
||||
poll.undecryptable
|
||||
? p({ class: "chat-poll-question" }, i18n.chatAccessDenied)
|
||||
: [
|
||||
p({ class: "chat-poll-question" }, poll.question),
|
||||
showResults ? renderResults(poll) : null,
|
||||
div({ class: "chat-poll-actions" },
|
||||
renderBallot(poll, `/chats/${encodeURIComponent(chat.key)}`, "/polls"),
|
||||
String(poll.author) === String(userId) && poll.status === "OPEN"
|
||||
? form({ method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.pollCloseButton)
|
||||
)
|
||||
: null
|
||||
),
|
||||
div({ class: "chat-poll-meta" },
|
||||
span({ class: "card-label" }, `${i18n.pollVoters}: `),
|
||||
span({ class: "card-value" }, String(poll.totalVoters)),
|
||||
span({ class: "card-label" }, ` · ${i18n.pollOutcome}: `),
|
||||
span({ class: "card-value" }, outcomeOf(poll))
|
||||
)
|
||||
],
|
||||
span({ class: "chat-bubble-time" }, moment(poll.createdAt).format("HH:mm"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const renderMessage = (msg, chatAuthor, ctx = {}) => {
|
||||
const isAuthor = String(msg.author) === String(chatAuthor)
|
||||
const isSelf = String(msg.author) === String(userId)
|
||||
const dateStr = moment(msg.createdAt).format("YYYY/MM/DD HH:mm")
|
||||
const shortId = msg.author ? "@" + msg.author.slice(1, 9) + "\u2026" : "?"
|
||||
const authorLink = msg.author ? userLink(msg.author) : span("?")
|
||||
|
||||
const imageNode = msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null
|
||||
|
||||
// m\u00f3vil: cita del mensaje respondido + bot\u00f3n \u21a9 para responder a ESTE mensaje (server-side ?replyTo=, sin JS)
|
||||
const mobile = process.env.OASIS_MOBILE === "1"
|
||||
const quoted = (mobile && msg.replyTo && msgById) ? msgById.get(String(msg.replyTo)) : null
|
||||
const quoteNode = quoted
|
||||
? div({ class: "chat-quote" },
|
||||
span({ class: "chat-quote-author" }, quoted.author ? userLink(quoted.author) : span("?")),
|
||||
span({ class: "chat-quote-text" }, String(quoted.text || (quoted.image ? "\ud83d\uddbc" : "")).slice(0, 80))
|
||||
)
|
||||
: null
|
||||
const replyLink = (mobile && chatKey && msg.key)
|
||||
? a({ class: "chat-reply-btn", href: `/chats/${encodeURIComponent(chatKey)}?replyTo=${encodeURIComponent(msg.key)}#chatbox`, title: "Reply" }, "\u21a9")
|
||||
// cita del mensaje respondido: se resuelve con el indice que arma la vista
|
||||
const quoted = (msg.replyTo && ctx.msgById) ? ctx.msgById.get(String(msg.replyTo)) : null
|
||||
const replyHref = (ctx.chatKey && msg.key)
|
||||
? `/chats/${encodeURIComponent(ctx.chatKey)}?replyTo=${encodeURIComponent(msg.key)}#chat-compose`
|
||||
: null
|
||||
const imageSrc = blobSrcOf(msg.image)
|
||||
const imageNode = imageSrc
|
||||
? renderZoomableImage(imageSrc, { imgClass: "chat-message-image" })
|
||||
: (msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null)
|
||||
|
||||
return div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" },
|
||||
div({ class: "chat-message-meta" },
|
||||
span({ class: "chat-message-sender" }, authorLink),
|
||||
span({ class: "chat-message-date" }, ` [ ${dateStr} ]`),
|
||||
replyLink
|
||||
),
|
||||
quoteNode,
|
||||
imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null,
|
||||
renderMessageText(msg.text || "")
|
||||
return div({ class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" },
|
||||
div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" },
|
||||
isSelf ? null : renderSenderLink(msg.author, isAuthor),
|
||||
quoted
|
||||
? div({ class: "chat-quote" },
|
||||
span({ class: "chat-quote-author" }, String(quoted.author || "").slice(0, 10)),
|
||||
span({ class: "chat-quote-text" }, String(quoted.text || "").slice(0, 120))
|
||||
)
|
||||
: null,
|
||||
renderMessageText(msg.text || ""),
|
||||
imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null,
|
||||
span({ class: "chat-bubble-time" }, moment(msg.createdAt).format("HH:mm")),
|
||||
replyHref ? a({ class: "chat-reply-btn", href: replyHref, title: i18n.chatReply || "Reply" }, "↩") : null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -192,15 +266,21 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => {
|
|||
|
||||
const isForm = filter === "create" || filter === "edit"
|
||||
|
||||
const chatHeaderMap = {
|
||||
all: i18n.chatsTitle,
|
||||
mine: i18n.chatMineSectionTitle || "Your Chats",
|
||||
recent: i18n.chatRecentTitle || "Recent Chats",
|
||||
favorites: i18n.chatFavoritesTitle || "Favorites",
|
||||
open: i18n.chatOpenTitle || "Open Chats",
|
||||
closed: i18n.chatClosedTitle || "Closed Chats"
|
||||
const headerText = i18n.chatsTitle
|
||||
|
||||
if (params.workspace && !isForm) {
|
||||
return template(
|
||||
i18n.chatsTitle,
|
||||
section({ class: "chat-workspace-section" },
|
||||
div({ class: "chat-workspace" },
|
||||
renderChatTopics(list, null),
|
||||
div({ class: "chat-workspace-main chat-workspace-empty" },
|
||||
p({ class: "chat-no-messages" }, list.length ? i18n.chatPickChat : i18n.chatNoneYet)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
const headerText = chatHeaderMap[filter] || i18n.chatsTitle
|
||||
|
||||
return template(
|
||||
i18n.chatsTitle,
|
||||
|
|
@ -214,12 +294,12 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => {
|
|||
!isForm
|
||||
? section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/chats" },
|
||||
form({ method: "GET", action: "/chats", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q }),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.search),
|
||||
br()
|
||||
input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -229,7 +309,7 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => {
|
|||
? renderChatForm(filter, filter === "edit" ? (chatToEdit || {}) : {}, params)
|
||||
: div({ class: "tribe-grid" },
|
||||
list.length
|
||||
? list.map(chat => renderChatCard(chat, filter, { q }))
|
||||
? list.map(chat => renderChatCard(chat, filter, { q, spread: params.spreadMap && params.spreadMap.get(chat.key) }))
|
||||
: p(i18n.chatNoItems)
|
||||
)
|
||||
)
|
||||
|
|
@ -241,7 +321,6 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q })
|
||||
const isAuthor = String(chat.author) === String(userId)
|
||||
const isMember = safeArr(chat.members).includes(userId) || (!!chat.tribeId && !!chat.isTribeMember)
|
||||
const fullShareUrl = `/chats/${encodeURIComponent(chat.key)}`
|
||||
const isRestrictedInviteOnly = !isMember && !isAuthor && chat.status === "INVITE-ONLY"
|
||||
|
||||
const statusLabel = chat.status === "CLOSED" ? i18n.chatStatusClosed :
|
||||
|
|
@ -253,19 +332,14 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
renderLifespanChip(chat.lifetime, i18n)
|
||||
].filter(Boolean)
|
||||
const chatSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(chat.key, null, { spread: params.spreads || null, author: chat.author, favKind: 'chats', isFavorite: chat.isFavorite, returnTo: params.returnTo, reportTitle: chat.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, chat.title || i18n.chatUntitled)
|
||||
),
|
||||
detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null,
|
||||
renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-detail-image" }),
|
||||
div({ class: "shop-share" },
|
||||
span({ class: "tribe-info-label" }, `${i18n.chatShareUrl}: `),
|
||||
input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" })
|
||||
),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(chat.key, params.spreads);
|
||||
return btn ? div({ class: "card-spread-centered" }, btn) : null;
|
||||
})(),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`)
|
||||
),
|
||||
|
|
@ -274,17 +348,18 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
td({ class: "tribe-info-label" }, i18n.chatCreatedAt),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, moment(chat.createdAt).format("YYYY/MM/DD HH:mm"))
|
||||
),
|
||||
!isRestrictedInviteOnly && chat.category ? tr(
|
||||
td({ class: "tribe-info-label" }, i18n.chatCategoryLabel),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category))
|
||||
) : null,
|
||||
isRestrictedInviteOnly ? null : tr(
|
||||
td({ class: "tribe-info-value", colspan: "4" },
|
||||
userLink(chat.author)
|
||||
)
|
||||
),
|
||||
!isRestrictedInviteOnly && chat.category ? tr(
|
||||
td({ class: "tribe-info-label" }, i18n.chatCategoryLabel),
|
||||
td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category))
|
||||
) : null
|
||||
)
|
||||
),
|
||||
isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" },
|
||||
isRestrictedInviteOnly ? null : (() => {
|
||||
const sideActionNodes = [
|
||||
isAuthor && chat.status === "INVITE-ONLY"
|
||||
? form({ method: "POST", action: `/chats/generate-invite` },
|
||||
input({ type: "hidden", name: "chatId", value: chat.key }),
|
||||
|
|
@ -310,39 +385,34 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation)
|
||||
)
|
||||
})(),
|
||||
form(
|
||||
{ method: "POST", action: chat.isFavorite ? `/chats/favorites/remove/${encodeURIComponent(chat.key)}` : `/chats/favorites/add/${encodeURIComponent(chat.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, chat.isFavorite ? i18n.chatRemoveFavorite : i18n.chatAddFavorite)
|
||||
),
|
||||
chat.author && String(chat.author) !== String(userId)
|
||||
? form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: chat.author }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.chatPM || i18n.privateMessage)
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate)
|
||||
)
|
||||
: null,
|
||||
isAuthor && chat.status !== "CLOSED"
|
||||
? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose)
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "POST", action: `/chats/delete/${encodeURIComponent(chat.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete)
|
||||
)
|
||||
: null,
|
||||
!isAuthor && isMember
|
||||
? form({ method: "POST", action: `/chats/leave/${encodeURIComponent(chat.key)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeLeaveButton)
|
||||
)
|
||||
: null
|
||||
].flat().filter(Boolean)
|
||||
return sideActionNodes.length ? div({ class: "tribe-side-actions" }, ...sideActionNodes) : null
|
||||
})(),
|
||||
isAuthor
|
||||
? div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.chatStatusLabel}: `),
|
||||
renderStateChip(chat.status === "CLOSED" ? "hidden" : "mutuals", chat.status === "CLOSED" ? "🔒" : "👁", String(chat.status || "OPEN").toUpperCase()),
|
||||
chat.status !== "CLOSED"
|
||||
? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}`, class: "inline-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose)
|
||||
)
|
||||
: null
|
||||
)
|
||||
: null,
|
||||
!isAuthor ? null : div({ class: "tribe-side-actions owner-actions" },
|
||||
form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate)
|
||||
),
|
||||
form({ method: "POST", action: `/chats/delete/${encodeURIComponent(chat.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete)
|
||||
)
|
||||
),
|
||||
!isMember && chat.status === "INVITE-ONLY"
|
||||
? div({ class: "chat-join-section" },
|
||||
|
|
@ -357,54 +427,104 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
)
|
||||
|
||||
const msgList = safeArr(messages)
|
||||
const msgById = new Map(msgList.map(m => [String(m.key), m]))
|
||||
const replyingTo = (process.env.OASIS_MOBILE === "1" && params.replyTo) ? msgById.get(String(params.replyTo)) : null
|
||||
// indice por clave para resolver las citas; se arma antes de mezclar con encuestas
|
||||
const msgById = new Map(msgList.filter(m => m && m.key).map(m => [String(m.key), m]))
|
||||
const replyingTo = params.replyTo ? msgById.get(String(params.replyTo)) : null
|
||||
const canWrite = (isMember || chat.status === "OPEN") && chat.status !== "CLOSED"
|
||||
|
||||
const chatMain = isRestrictedInviteOnly
|
||||
? div({ class: "tribe-main chat-full-width" }, p({ class: "access-denied-msg" }, i18n.chatAccessDenied))
|
||||
: div({ class: "tribe-main chat-full-width" },
|
||||
canWrite
|
||||
? div({ class: "chat-message-form", id: "chatbox" },
|
||||
replyingTo
|
||||
? div({ class: "chat-replying-to" },
|
||||
span({ class: "chat-replying-label" }, "Replying to: "),
|
||||
span({ class: "chat-quote-author" }, replyingTo.author ? userLink(replyingTo.author) : span("?")),
|
||||
span({ class: "chat-quote-text" }, String(replyingTo.text || (replyingTo.image ? "🖼" : "")).slice(0, 80)),
|
||||
a({ class: "chat-reply-cancel", href: `/chats/${encodeURIComponent(chat.key)}`, title: "Cancel" }, "✕")
|
||||
)
|
||||
: null,
|
||||
form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/message`, enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }),
|
||||
replyingTo ? input({ type: "hidden", name: "replyTo", value: String(params.replyTo) }) : null,
|
||||
textarea({ name: "text", rows: 3, placeholder: i18n.chatMessagePlaceholder }), br(),
|
||||
span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(),
|
||||
input({ type: "file", name: "image", accept: "image/*" }), br(), br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.chatSendMessage)
|
||||
)
|
||||
msgList.length
|
||||
? div({ class: "chat-jump-row" },
|
||||
a({ href: "#chat-latest", class: "filter-btn chat-jump-latest" }, i18n.chatJumpLatest)
|
||||
)
|
||||
: null,
|
||||
div({ class: "chat-messages-list" },
|
||||
(() => {
|
||||
const visible = msgList.filter(msg => (msg.text && String(msg.text).trim()) || msg.image)
|
||||
return visible.length
|
||||
? visible.map(msg => renderMessage(msg, chat.author, chat.key, msgById))
|
||||
: p({ class: "chat-no-messages" }, i18n.chatNoMessages)
|
||||
const visible = msgList
|
||||
.filter(msg => (msg.text && String(msg.text).trim()) || msg.image)
|
||||
.map(msg => ({ kind: 'message', ts: Date.parse(msg.createdAt) || 0, msg }))
|
||||
const pollItems = safeArr(params.polls).map(poll => ({
|
||||
kind: 'poll', ts: Date.parse(poll.createdAt) || 0, poll
|
||||
}))
|
||||
const stream = [...visible, ...pollItems].sort((a, b) => a.ts - b.ts)
|
||||
if (!stream.length) return p({ class: "chat-no-messages" }, i18n.chatNoMessages)
|
||||
const nodes = []
|
||||
let prevDay = null
|
||||
stream.forEach((entry, i) => {
|
||||
const day = moment(entry.ts).format("YYYY/MM/DD")
|
||||
if (day !== prevDay) {
|
||||
nodes.push(div({ class: "chat-day-separator" }, span({ class: "chat-day-chip" }, day)))
|
||||
prevDay = day
|
||||
}
|
||||
const last = i === stream.length - 1
|
||||
const node = entry.kind === 'poll'
|
||||
? renderChatPoll(entry.poll, chat)
|
||||
: renderMessage(entry.msg, chat.author, { msgById, chatKey: chat.key })
|
||||
nodes.push(last ? div({ id: "chat-latest", class: "chat-latest-anchor" }, node) : node)
|
||||
})
|
||||
return nodes
|
||||
})()
|
||||
)
|
||||
),
|
||||
canWrite
|
||||
? div({ class: "chat-message-form" },
|
||||
form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/message`, enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }),
|
||||
replyingTo
|
||||
? div({ class: "chat-replying-to", id: "chat-compose" },
|
||||
span({ class: "chat-quote-author" }, String(replyingTo.author || "").slice(0, 10)),
|
||||
span({ class: "chat-quote-text" }, String(replyingTo.text || "").slice(0, 120)),
|
||||
a({ class: "chat-reply-cancel", href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, "✕")
|
||||
)
|
||||
: null,
|
||||
replyingTo ? input({ type: "hidden", name: "replyTo", value: String(params.replyTo) }) : null,
|
||||
textarea({ name: "text", rows: 3, placeholder: i18n.chatMessagePlaceholder }), br(),
|
||||
span(i18n.uploadMedia), br(),
|
||||
input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.chatSendMessage)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
canWrite && params.pollsEnabled
|
||||
? details({ class: "chat-poll-form" },
|
||||
summary({ class: "chat-poll-summary" }, i18n.pollChatCreate),
|
||||
form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/polls/create` },
|
||||
input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }),
|
||||
span(i18n.pollQuestion), br(),
|
||||
input({ type: "text", name: "question", required: true, maxlength: "300", placeholder: i18n.pollQuestionPlaceholder }), br(), br(),
|
||||
span(i18n.pollOptions), br(),
|
||||
textarea({ name: "options", rows: 4, required: true, placeholder: i18n.pollOptionsPlaceholder }), br(), br(),
|
||||
div({ class: "poll-switch" },
|
||||
input({ type: "hidden", name: "anonymous", value: "0" }),
|
||||
label(input({ type: "checkbox", name: "anonymous", value: "1" }), " ", i18n.pollAnonymousLabel)
|
||||
),
|
||||
div({ class: "poll-switch" },
|
||||
input({ type: "hidden", name: "multiple", value: "0" }),
|
||||
label(input({ type: "checkbox", name: "multiple", value: "1" }), " ", i18n.pollMultipleLabel)
|
||||
),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.pollPublishButton)
|
||||
)
|
||||
)
|
||||
: null
|
||||
)
|
||||
|
||||
// móvil: tira para saltar entre chats sin volver a la lista (server-side, enlaces)
|
||||
const switcherChats = process.env.OASIS_MOBILE === "1" && Array.isArray(params.chatList) ? params.chatList : []
|
||||
const chatSwitcher = switcherChats.length
|
||||
? div({ class: "chat-switcher" },
|
||||
switcherChats.slice(0, 30).map(c => a({
|
||||
class: String(c.key) === String(chat.key) ? "chat-switch-chip active" : "chat-switch-chip",
|
||||
href: `/chats/${encodeURIComponent(c.key)}`,
|
||||
title: c.title || ""
|
||||
}, (c.title || "·").slice(0, 18)))
|
||||
if (params.workspace) {
|
||||
return template(
|
||||
chat.title || i18n.chatUntitled,
|
||||
section({ class: "chat-workspace-section" },
|
||||
div({ class: "chat-workspace" },
|
||||
renderChatTopics(safeArr(params.allChats), chat.rootId || chat.key),
|
||||
div({ class: "chat-workspace-main" },
|
||||
div({ class: "tribe-details chat-workspace-details" },
|
||||
chatSide,
|
||||
chatMain
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
return template(
|
||||
chat.title || i18n.chatUntitled,
|
||||
|
|
@ -413,8 +533,7 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => {
|
|||
h2(i18n.chatsTitle),
|
||||
p(i18n.modulesChatsDescription)
|
||||
),
|
||||
renderModeButtons(filter || "all"),
|
||||
chatSwitcher
|
||||
renderModeButtons(filter || "all")
|
||||
),
|
||||
section(
|
||||
div({ class: "tribe-details" },
|
||||
|
|
|
|||
|
|
@ -470,11 +470,11 @@ const CaseCard = (c) => {
|
|||
select(
|
||||
{ name: 'preference' },
|
||||
option(
|
||||
{ value: 'YES', selected: c.myPublicPreference === true },
|
||||
{ value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})},
|
||||
i18n.courtsPublicPrefYes
|
||||
),
|
||||
option(
|
||||
{ value: 'NO', selected: c.myPublicPreference === false },
|
||||
{ value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})},
|
||||
i18n.courtsPublicPrefNo
|
||||
)
|
||||
),
|
||||
|
|
@ -685,11 +685,11 @@ const MyCaseCard = (c) => {
|
|||
select(
|
||||
{ name: 'preference' },
|
||||
option(
|
||||
{ value: 'YES', selected: c.myPublicPreference === true },
|
||||
{ value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})},
|
||||
i18n.courtsPublicPrefYes
|
||||
),
|
||||
option(
|
||||
{ value: 'NO', selected: c.myPublicPreference === false },
|
||||
{ value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})},
|
||||
i18n.courtsPublicPrefNo
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ module.exports = ({ i18n }) => {
|
|||
const pair = [ bbLink("/peers", "⧖", i18n.peers || "Peers") ];
|
||||
if (modOn('invitesMod')) pair.push(bbLink("/invites", "ꔮ", i18n.invites || "Invites"));
|
||||
return nav(
|
||||
{ class: "oasis-bottombar", "aria-label": i18n.bbQuickActions || "Quick actions" },
|
||||
{ class: "oasis-bottombar", attrs: { "aria-label": i18n.bbQuickActions || "Quick actions" } },
|
||||
...slots,
|
||||
editSlot,
|
||||
div({ class: "bb-pair" }, ...pair)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const {
|
|||
input, label, br, select, option, h2, textarea
|
||||
} = require("../server/node_modules/hyperaxe");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip } = require('./main_views');
|
||||
const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions } = require('./main_views');
|
||||
const { renderEncryptedChip: renderForumEncryptedChip } = require('./clearnet_view');
|
||||
const { config } = require('../server/SSB_server.js');
|
||||
const { renderUrl } = require('../backend/renderUrl');
|
||||
|
|
@ -23,8 +23,8 @@ exports.renderForumInvitePage = (code) => {
|
|||
};
|
||||
|
||||
const BASE_FILTERS = ['hot','all','mine','recent','top'];
|
||||
const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.','POLITICS','TECH'];
|
||||
const CAT_BLOCK2 = ['SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS'];
|
||||
const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.'];
|
||||
const CAT_BLOCK2 = ['POLITICS','TECH','SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS'];
|
||||
const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM'];
|
||||
const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3];
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ const renderForumForm = () =>
|
|||
label(i18n.forumTitleLabel), br(),
|
||||
input({
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
name: 'title', maxlength: '100',
|
||||
required: true,
|
||||
placeholder: i18n.forumTitlePlaceholder
|
||||
}), br(), br(),
|
||||
|
|
@ -138,7 +138,7 @@ const renderThread = (nodes, level = 0, forumId) => {
|
|||
userLink(m.author),
|
||||
div({ class: 'comment-votes' },
|
||||
span({ class: 'votes-count' }, `▲: ${m.positiveVotes || 0}`),
|
||||
span({ class: 'votes-count', style: 'margin-left:12px;' },
|
||||
span({ class: 'votes-count votes-count-negative' },
|
||||
`▼: ${m.negativeVotes || 0}`)
|
||||
)
|
||||
),
|
||||
|
|
@ -190,6 +190,9 @@ const renderForumList = (forums, currentFilter, spreadMap = new Map()) => {
|
|||
renderVotes(f.key, f.score, f.key)
|
||||
),
|
||||
div({ class: 'forum-main-col' },
|
||||
div({ class: 'card-header activity-card-header' },
|
||||
renderContentActions(f.key, `/forum/${encodeURIComponent(f.key)}`, { spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title })
|
||||
),
|
||||
div({ class: 'forum-header-row' },
|
||||
a({
|
||||
class: 'forum-category',
|
||||
|
|
@ -210,20 +213,13 @@ const renderForumList = (forums, currentFilter, spreadMap = new Map()) => {
|
|||
div({ class: 'forum-meta' },
|
||||
span({ class: 'forum-positive-votes' },
|
||||
`▲: ${f.positiveVotes || 0}`),
|
||||
span({ class: 'forum-negative-votes', style: 'margin-left:12px;' },
|
||||
span({ class: 'forum-negative-votes' },
|
||||
`▼: ${f.negativeVotes || 0}`),
|
||||
span({ class: 'forum-participants' },
|
||||
`${i18n.forumParticipants.toUpperCase()}: ${f.participants?.length || 1}`),
|
||||
span({ class: 'forum-messages' },
|
||||
`${i18n.forumMessages.toUpperCase()}: ${(f.messagesCount || 1) - 1}`),
|
||||
form({ method: 'GET', action: `/forum/${encodeURIComponent(f.key)}`, class: 'visit-forum-form' },
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton)
|
||||
)
|
||||
`${i18n.forumMessages.toUpperCase()}: ${(f.messagesCount || 1) - 1}`)
|
||||
),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(f.key, spreadMap.get(f.key));
|
||||
return btn ? div({ class: 'card-spread-left' }, btn) : null;
|
||||
})(),
|
||||
div({ class: 'forum-footer' },
|
||||
span({ class: 'date-link' },
|
||||
`${moment(f.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`),
|
||||
|
|
@ -253,9 +249,7 @@ exports.forumView = async (forums, currentFilter, params = {}) => {
|
|||
return template(i18n.forumTitle,
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
h2(currentFilter === 'create'
|
||||
? i18n.forumCreateSectionTitle
|
||||
: i18n.forumTitle),
|
||||
h2(i18n.forumTitle),
|
||||
p(i18n.forumDescription)
|
||||
),
|
||||
div({ class: 'mode-buttons-cols' },
|
||||
|
|
@ -271,6 +265,17 @@ exports.forumView = async (forums, currentFilter, params = {}) => {
|
|||
generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP),
|
||||
renderCreateForumButton()
|
||||
),
|
||||
currentFilter === 'create'
|
||||
? null
|
||||
: div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/forum', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: currentFilter || 'all' }),
|
||||
input({ type: 'text', name: 'q', value: params.q || '', placeholder: i18n.forumSearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
currentFilter === 'create'
|
||||
? renderForumForm()
|
||||
: renderForumList(
|
||||
|
|
@ -282,20 +287,8 @@ exports.forumView = async (forums, currentFilter, params = {}) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.singleForumView = async (forum, messagesData, currentFilter, topic = '') => {
|
||||
exports.singleForumView = async (forum, messagesData, currentFilter) => {
|
||||
const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{});
|
||||
// móvil: subtopics — etiqueta opcional en los mensajes raíz → tira de pestañas + filtro por ?topic=
|
||||
const forumMobile = process.env.OASIS_MOBILE === '1';
|
||||
const rootMsgs = Array.isArray(messagesData.messages) ? messagesData.messages : [];
|
||||
const forumTopics = forumMobile ? [...new Set(rootMsgs.map(m => String(m.subtopic || '').trim()).filter(Boolean))] : [];
|
||||
const activeTopic = forumMobile ? String(topic || '').trim() : '';
|
||||
const shownMsgs = activeTopic ? rootMsgs.filter(m => String(m.subtopic || '').trim() === activeTopic) : rootMsgs;
|
||||
const topicStrip = (forumMobile && forumTopics.length)
|
||||
? div({ class: 'forum-topics' },
|
||||
a({ class: activeTopic ? 'forum-topic-chip' : 'forum-topic-chip active', href: `/forum/${encodeURIComponent(forum.key)}` }, i18n.menuAll || 'All'),
|
||||
...forumTopics.map(t => a({ class: t === activeTopic ? 'forum-topic-chip active' : 'forum-topic-chip', href: `/forum/${encodeURIComponent(forum.key)}?topic=${encodeURIComponent(t)}` }, t))
|
||||
)
|
||||
: null;
|
||||
return template(forum.title,
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
|
|
@ -317,23 +310,22 @@ exports.singleForumView = async (forum, messagesData, currentFilter, topic = '')
|
|||
)
|
||||
),
|
||||
div({ class: 'forum-thread-container' },
|
||||
topicStrip,
|
||||
div({
|
||||
class: 'forum-card forum-thread-header',
|
||||
style: 'display:flex;align-items:flex-start;'
|
||||
},
|
||||
div({
|
||||
class: 'root-vote-col',
|
||||
style: 'width:60px;text-align:center;'
|
||||
}, renderVotes(
|
||||
div({ class: 'forum-card forum-thread-header' },
|
||||
div({ class: 'root-vote-col' }, renderVotes(
|
||||
forum.key,
|
||||
messagesData.totalScore,
|
||||
forum.key
|
||||
)),
|
||||
div({
|
||||
class: 'forum-main-col',
|
||||
style: 'flex:1;padding-left:10px;'
|
||||
},
|
||||
div({ class: 'forum-main-col' },
|
||||
div({ class: 'card-header activity-card-header' },
|
||||
renderContentActions(forum.key, null, {
|
||||
author: forum.author,
|
||||
favKind: 'forum',
|
||||
isFavorite: forum.isFavorite,
|
||||
spread: null,
|
||||
reportTitle: forum.title
|
||||
})
|
||||
),
|
||||
div({ class: 'forum-header-row' },
|
||||
a({
|
||||
class: 'forum-category',
|
||||
|
|
@ -376,21 +368,15 @@ exports.singleForumView = async (forum, messagesData, currentFilter, topic = '')
|
|||
innerHTML: sanitizeHtml(renderTextWithStyles(forum.text || ''))
|
||||
}),
|
||||
div({ class: 'forum-meta' },
|
||||
span({ class: 'votes-count' },
|
||||
span({ class: 'forum-positive-votes' },
|
||||
`▲: ${messagesData.positiveVotes}`),
|
||||
span({
|
||||
class: 'votes-count',
|
||||
style: 'margin-left:12px;'
|
||||
}, `▼: ${messagesData.negativeVotes}`),
|
||||
span({ class: 'forum-negative-votes' },
|
||||
`▼: ${messagesData.negativeVotes}`),
|
||||
span({ class: 'forum-participants' },
|
||||
`${i18n.forumParticipants.toUpperCase()}: ${forum.participants?.length || 1}`),
|
||||
span({ class: 'forum-messages' },
|
||||
`${i18n.forumMessages.toUpperCase()}: ${messagesData.total}`)
|
||||
),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(forum.key);
|
||||
return btn ? div({ class: 'card-spread-left' }, btn) : null;
|
||||
})(),
|
||||
div({ class: 'forum-footer' },
|
||||
span({ class: 'date-link' },
|
||||
`${moment(forum.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`),
|
||||
|
|
@ -398,31 +384,23 @@ exports.singleForumView = async (forum, messagesData, currentFilter, topic = '')
|
|||
)
|
||||
)
|
||||
),
|
||||
div({
|
||||
class: 'new-message-wrapper',
|
||||
style: 'margin-top:12px;'
|
||||
},
|
||||
div({ class: 'new-message-wrapper' },
|
||||
form({
|
||||
method: 'POST',
|
||||
action: `/forum/${encodeURIComponent(forum.key)}/message`,
|
||||
class: 'new-message-form'
|
||||
},
|
||||
forumMobile ? input({ type: 'text', name: 'subtopic', value: activeTopic, placeholder: 'Topic (optional)', class: 'forum-topic-input' }) : null,
|
||||
textarea({
|
||||
name: 'message',
|
||||
rows: 4,
|
||||
required: true,
|
||||
placeholder: i18n.forumMessagePlaceholder,
|
||||
style: 'width:100%;'
|
||||
class: 'new-message-textarea'
|
||||
}), br(),
|
||||
button({
|
||||
type: 'submit',
|
||||
class: 'forum-send-btn',
|
||||
style: 'margin-top:4px;'
|
||||
}, i18n.forumSendButton)
|
||||
button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton)
|
||||
)
|
||||
),
|
||||
...renderThread(shownMsgs, 0, forum.key)
|
||||
...renderThread(messagesData.messages, 0, forum.key)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
contentTypes.map(type =>
|
||||
option({
|
||||
value: type === 'all' ? "" : type,
|
||||
selected: (types.length === 0 && type === 'all') || types.includes(type)
|
||||
...((types.length === 0 && type === 'all') || types.includes(type) ? { selected: true } : {})
|
||||
}, i18n[type + "Label"] || type.toUpperCase())
|
||||
)
|
||||
);
|
||||
|
|
@ -89,6 +89,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'market': return `/market/${encodeURIComponent(contentId)}`;
|
||||
case 'report': return `/reports/${encodeURIComponent(contentId)}`;
|
||||
case 'project': return `/projects/${encodeURIComponent(contentId)}`;
|
||||
case 'housing': return `/housing/${encodeURIComponent(contentId)}`;
|
||||
case 'job': return `/jobs/${encodeURIComponent(contentId)}`;
|
||||
case 'industry': return `/industry/${encodeURIComponent(contentId)}`;
|
||||
case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(contentId)}`;
|
||||
|
|
@ -444,6 +445,14 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${content.txid}`, target: '_blank' }, content.txid)
|
||||
) : null
|
||||
);
|
||||
case 'housing':
|
||||
return div({ class: 'search-housing' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
content.housing_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingType + ':'), span({ class: 'card-value' }, String(content.housing_type).toUpperCase())) : null,
|
||||
content.place ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPlace + ':'), span({ class: 'card-value' }, content.place)) : null,
|
||||
String(content.housing_type || '').toLowerCase() !== 'couchsurfing' && content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPrice + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null,
|
||||
content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingStatus + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : null
|
||||
);
|
||||
case 'job':
|
||||
return div({ class: 'search-job' },
|
||||
content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue