backend: backend.js a 0.9.5, con lo que faltaba para los modulos nuevos
backend.js pasa a la version de 0.9.5 y baja de 1549 a 16 lineas de divergencia. Sin esto los modulos que el dev ha anadido estaban portados pero inaccesibles: no tenian rutas. Ahora /polls, /blogs y /mentions responden. Reinyectado de la rama: el enganche de fork_routes, los tres gates de movil (no abrir navegador al arrancar, no matar el sbot al borrar la cadena), el selector de chats de la barra lateral movil, y los globals de la interfaz — el filtro Personal/Community, la visibilidad de los hexagonos y el avatar de la topbar. Esos globals no aparecian al clasificar el diff y su perdida dejaba la home sin hexagonos; detectado al comparar el HTML servido. bottomBarPickerView (la pantalla de personalizar la barra) vivia en main_views y se perdia al adoptar 0.9.5. Pasa a fork/hive_nav.js, junto al resto de la interfaz de la rama, y main_views la reexporta en una linea. housing se porta pero apagado (housingMod off). Mantenerlo fuera obligaba a editar 83 referencias de backend.js en cada release; asi el fichero queda igual que upstream y el modulo no aparece en la interfaz. Se portan tambien contentPdf, housing_model y housing_view para cerrar los requires. Verificado con la app en marcha: 29 rutas OK, hexagonos 10/47 con los filtros Personal y Community dando 5 y 5, topbar con avatar, 7 accesos rapidos, orden de hojas correcto, y Karvan creando sala y sirviendo el panel de videollamada.
This commit is contained in:
parent
7eeac8b004
commit
7b90fa8211
8 changed files with 2579 additions and 347 deletions
|
|
@ -7,7 +7,7 @@
|
|||
// cada integracion. Aqui solo depende de i18n, que se inyecta porque main_views
|
||||
// lo muta en cada peticion sin reasignarlo.
|
||||
|
||||
const { a, div, img, input, label, nav, span } = require("../../server/node_modules/hyperaxe");
|
||||
const { a, button, div, form, h2, h3, img, input, label, nav, p, section, span } = require("../../server/node_modules/hyperaxe");
|
||||
const { getConfig } = require("../../configs/config-manager.js");
|
||||
const sharedState = require("../../configs/shared-state");
|
||||
|
||||
|
|
@ -180,5 +180,59 @@ module.exports = ({ i18n }) => {
|
|||
return div({ class: 'hive-nav' }, ...radios, grid, ...panels);
|
||||
};
|
||||
|
||||
return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES };
|
||||
|
||||
// pantalla para personalizar la barra inferior (/settings/bottombar)
|
||||
const bottomBarPickerView = (template) => {
|
||||
const cfg = getConfig();
|
||||
const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined;
|
||||
const MAX = 4;
|
||||
const pinnedKeys = cfg.bottomBarPins || [];
|
||||
const pinned = pinnedKeys
|
||||
.map(key => PINNABLE_MODULES.find(m => m.key === key))
|
||||
.filter(Boolean);
|
||||
const full = pinned.length >= MAX;
|
||||
const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls },
|
||||
input({ type: 'hidden', name: 'action', value: action }),
|
||||
input({ type: 'hidden', name: 'key', value: key }),
|
||||
node
|
||||
);
|
||||
const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form',
|
||||
button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' },
|
||||
span({ class: 'bb-manage-ico' }, m.glyph),
|
||||
span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key),
|
||||
span({ class: 'bb-manage-x' }, '✕')
|
||||
)
|
||||
);
|
||||
const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form',
|
||||
button({ type: 'submit', class: 'bb-pick-btn' },
|
||||
span({ class: 'bb-pick-ico' }, m.glyph),
|
||||
span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key)
|
||||
)
|
||||
);
|
||||
const addable = PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key));
|
||||
return template(
|
||||
i18n.bbManage || 'Edit bar',
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
h2(i18n.bbPickTitle || 'Customize the bar'),
|
||||
p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.')
|
||||
),
|
||||
div({ class: 'bb-manage' },
|
||||
h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`),
|
||||
pinned.length
|
||||
? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow))
|
||||
: p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None')
|
||||
),
|
||||
div({ class: 'bb-manage-add' },
|
||||
h3(i18n.bbAdd || 'Add'),
|
||||
full
|
||||
? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached')
|
||||
: div({ class: 'bb-pick-grid' }, ...addable.map(addBtn))
|
||||
),
|
||||
a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done')
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES, bottomBarPickerView };
|
||||
};
|
||||
|
|
|
|||
496
src/views/housing_view.js
Normal file
496
src/views/housing_view.js
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, video, table, tr, td } = require("../server/node_modules/hyperaxe")
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, userLink, renderOpenClosedChip, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderOpinionsVoting, renderEngagement, renderSpreadEditWarning } = require("./main_views")
|
||||
const { blobUrl, blobIdOf, isVideoEntry, imagesOf, renderMediaThumb, renderPhotoGallery, renderGalleryFields } = require("./gallery_view")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
const opinionCategories = require("../backend/opinion_categories")
|
||||
const { renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view")
|
||||
|
||||
const userId = config.keys.id
|
||||
|
||||
const FILTERS = [
|
||||
{ key: "ALL", i18n: "housingFilterAll" },
|
||||
{ key: "RECENT", i18n: "housingFilterRecent" },
|
||||
{ key: "MINE", i18n: "housingFilterMine" },
|
||||
{ key: "TOP", i18n: "housingFilterTop" },
|
||||
{ key: "REQUESTED", i18n: "housingFilterRequested" },
|
||||
{ key: "SALE", i18n: "housingFilterSale" },
|
||||
{ key: "RENT", i18n: "housingFilterRent" },
|
||||
{ key: "COUCHSURFING", i18n: "housingFilterCouchsurfing" },
|
||||
{ key: "OPEN", i18n: "housingFilterOpen" },
|
||||
{ key: "CLOSED", i18n: "housingFilterClosed" }
|
||||
]
|
||||
|
||||
const MAX_IMAGES = 8
|
||||
|
||||
const safeArr = (v) => (Array.isArray(v) ? v : [])
|
||||
const safeText = (v) => String(v || "").trim()
|
||||
|
||||
const parseNum = (v) => {
|
||||
const n = parseFloat(String(v ?? "").replace(",", "."))
|
||||
return Number.isFinite(n) ? n : NaN
|
||||
}
|
||||
|
||||
const fmtPrice = (v) => {
|
||||
const n = parseNum(v)
|
||||
return Number.isFinite(n) ? n.toFixed(2) : String(v ?? "")
|
||||
}
|
||||
|
||||
|
||||
|
||||
const priceLabel = (item) => {
|
||||
if (item.housing_type === "couchsurfing") return i18n.housingFree || "FREE"
|
||||
const suffix = item.housing_type === "rent" ? ` ${i18n.housingPerMonth || "/month"}` : ""
|
||||
return `${fmtPrice(item.price)} ECO${suffix}`
|
||||
}
|
||||
|
||||
const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((s, c) => s + (Number((opinions || {})[c]) || 0), 0)
|
||||
|
||||
const renderStarRating = (opinions, voterCount) => {
|
||||
const pos = sumCats(opinions, opinionCategories.positive)
|
||||
const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation)
|
||||
const total = pos + neg
|
||||
const full = total > 0 ? Math.round((pos / total) * 5) : 0
|
||||
const stars = "★".repeat(full) + "☆".repeat(5 - full)
|
||||
return span({ class: "housing-stars" }, `${stars} (${voterCount})`)
|
||||
}
|
||||
|
||||
const buildReturnTo = (filter, params = {}) => {
|
||||
const parts = [`filter=${encodeURIComponent(safeText(filter || "ALL"))}`]
|
||||
const q = safeText(params.search || "")
|
||||
if (q) parts.push(`search=${encodeURIComponent(q)}`)
|
||||
if (String(params.minPrice ?? "") !== "") parts.push(`minPrice=${encodeURIComponent(String(params.minPrice))}`)
|
||||
if (String(params.maxPrice ?? "") !== "") parts.push(`maxPrice=${encodeURIComponent(String(params.maxPrice))}`)
|
||||
if (safeText(params.place)) parts.push(`place=${encodeURIComponent(safeText(params.place))}`)
|
||||
if (safeText(params.sort)) parts.push(`sort=${encodeURIComponent(safeText(params.sort))}`)
|
||||
return `/housing?${parts.join("&")}`
|
||||
}
|
||||
|
||||
const renderTags = (tags = []) => {
|
||||
const arr = safeArr(tags).map(t => String(t || "").trim()).filter(Boolean)
|
||||
return arr.length
|
||||
? div({ class: "card-tags" }, arr.map(tag => a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` }, `#${tag}`)))
|
||||
: null
|
||||
}
|
||||
|
||||
const renderTypeChip = (item) => {
|
||||
const t = String(item.housing_type || "").toUpperCase()
|
||||
const emoji = item.housing_type === "sale" ? "🏷" : item.housing_type === "rent" ? "🔑" : "🛋"
|
||||
return renderStateChip("whole", emoji, i18n["housingType" + t] || t)
|
||||
}
|
||||
|
||||
const renderStatusChip = (status) => {
|
||||
const s = String(status || "").toUpperCase()
|
||||
return renderOpenClosedChip(s, {
|
||||
statusChipOPEN: i18n.housingStatusOPEN || "OPEN",
|
||||
statusChipCLOSED: i18n.housingStatusCLOSED || "CLOSED"
|
||||
})
|
||||
}
|
||||
|
||||
const renderRequestedChip = () => renderStateChip("whole", "★", i18n.housingRequestedBadge || "REQUESTED")
|
||||
|
||||
const renderInfoTable = (item) => {
|
||||
const rows = []
|
||||
const pushRow = (labelText, valueNode, valueClass) =>
|
||||
rows.push(tr(
|
||||
td({ class: "tribe-info-label" }, labelText),
|
||||
td({ class: `tribe-info-value${valueClass ? " " + valueClass : ""}` }, valueNode)
|
||||
))
|
||||
pushRow(i18n.housingProperty, i18n["housingProperty" + String(item.property_type || "").toUpperCase()] || "—")
|
||||
pushRow(i18n.housingPrice, priceLabel(item), "card-salary")
|
||||
if (item.rooms > 0) pushRow(i18n.housingRooms, String(item.rooms))
|
||||
if (item.size > 0) pushRow(i18n.housingSize, `${item.size} m²`)
|
||||
if (item.capacity > 0) pushRow(i18n.housingCapacity, String(item.capacity))
|
||||
if (item.availableFrom) pushRow(i18n.housingAvailableFrom, moment(item.availableFrom).format("YYYY/MM/DD"))
|
||||
if (item.availableTo) pushRow(i18n.housingAvailableTo, moment(item.availableTo).format("YYYY/MM/DD"))
|
||||
if (safeText(item.place)) pushRow(i18n.housingPlace, safeText(item.place))
|
||||
if (item.mapUrl) {
|
||||
const mapNode = renderMapLocationVisitLabel(item.mapUrl)
|
||||
if (mapNode) pushRow(i18n.mapLocationTitle || "Map", mapNode)
|
||||
}
|
||||
return table({ class: "tribe-info-table housing-info-table" }, ...rows)
|
||||
}
|
||||
|
||||
const today = () => moment().format("YYYY-MM-DD")
|
||||
|
||||
const renderStatusRow = (item, returnTo) => {
|
||||
if (String(item.author) !== String(userId)) return null
|
||||
const isOpen = item.status === "OPEN"
|
||||
return div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.housingStatus}: `),
|
||||
renderStatusChip(item.status),
|
||||
form({ method: "POST", action: `/housing/status/${encodeURIComponent(item.id)}`, class: "inline-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "tribe-action-btn", type: "submit", name: "status", value: isOpen ? "CLOSED" : "OPEN" },
|
||||
isOpen ? (i18n.housingSetClosed || "Close") : (i18n.housingSetOpen || "Reopen"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const renderOwnerActions = (item, returnTo) => {
|
||||
if (String(item.author) !== String(userId)) return []
|
||||
return [
|
||||
form({ method: "GET", action: `/housing/edit/${encodeURIComponent(item.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "update-btn", type: "submit" }, i18n.housingUpdateButton)
|
||||
),
|
||||
form({ method: "POST", action: `/housing/delete/${encodeURIComponent(item.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ class: "delete-btn", type: "submit" }, i18n.housingDeleteButton)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
const renderRequestToggle = (item, returnTo) => {
|
||||
if (String(item.author) === String(userId)) return null
|
||||
if (item.status !== "OPEN") return null
|
||||
const requested = safeArr(item.requests).includes(userId)
|
||||
return requested
|
||||
? form({ method: "POST", action: `/housing/cancel/${encodeURIComponent(item.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.housingCancelButton)
|
||||
)
|
||||
: form({ method: "POST", action: `/housing/request/${encodeURIComponent(item.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.housingRequestButton)
|
||||
)
|
||||
}
|
||||
|
||||
const renderHousingList = (items, filter, params = {}) => {
|
||||
const list = safeArr(items)
|
||||
if (!list.length) return p(i18n.housingNoItems)
|
||||
|
||||
return div({ class: "housing-grid" },
|
||||
list.map((item) => {
|
||||
const clip = safeText(item.video)
|
||||
const cover = imagesOf(item)[0]
|
||||
const isOwn = item.author && String(item.author) === String(userId)
|
||||
const requested = safeArr(item.requests).includes(userId)
|
||||
const chips = [
|
||||
renderTypeChip(item),
|
||||
renderStatusChip(item.status),
|
||||
item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null,
|
||||
requested ? renderRequestedChip() : null,
|
||||
renderLifespanChip(item.lifetime, i18n)
|
||||
].filter(Boolean)
|
||||
|
||||
return div({ class: "trending-card housing-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreadMap && params.spreadMap.get(item.id) || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title })
|
||||
),
|
||||
div({ class: "card-section housing-card-body" },
|
||||
clip || (cover && isVideoEntry(cover))
|
||||
? div({ class: "tribe-card-image-wrapper housing-card-video" },
|
||||
video({ controls: true, class: "housing-card-hero-video", src: blobUrl(blobIdOf(clip || cover)) })
|
||||
)
|
||||
: cover
|
||||
? div({ class: "tribe-card-image-wrapper" },
|
||||
a({ href: `/housing/${encodeURIComponent(item.id)}` },
|
||||
img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-card-hero-image", alt: "" })
|
||||
)
|
||||
)
|
||||
: null,
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" },
|
||||
a({ href: `/housing/${encodeURIComponent(item.id)}` }, safeText(item.title) || i18n.housingTitle)
|
||||
)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
renderStarRating(item.opinions, safeArr(item.opinions_inhabitants).length),
|
||||
div({ class: "card-date-highlight" }, priceLabel(item)),
|
||||
safeText(item.place)
|
||||
? div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.housingPlace}: `),
|
||||
span({ class: "card-value" }, safeText(item.place))
|
||||
)
|
||||
: null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`)
|
||||
),
|
||||
imagesOf(item).length > 1
|
||||
? div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.galleryPhotos}: `),
|
||||
span({ class: "card-value" }, String(imagesOf(item).length))
|
||||
)
|
||||
: null
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const renderHousingForm = (item = {}, mode = "create", maxImages = MAX_IMAGES, spreadWarning = null) => {
|
||||
const isEdit = mode === "edit"
|
||||
const type = String(item.housing_type || "").toLowerCase()
|
||||
return div({ class: "div-center housing-form" },
|
||||
form({
|
||||
action: isEdit ? `/housing/update/${encodeURIComponent(item.id)}` : "/housing/create",
|
||||
method: "POST",
|
||||
enctype: "multipart/form-data"
|
||||
},
|
||||
input({ type: "hidden", name: "returnTo", value: "/housing?filter=MINE" }),
|
||||
isEdit ? spreadWarning : null,
|
||||
label(i18n.housingType),
|
||||
br(),
|
||||
select({ name: "housing_type", required: true },
|
||||
option({ value: "sale", selected: type === "sale" ? "selected" : undefined }, i18n.housingTypeSALE),
|
||||
option({ value: "rent", selected: type === "rent" ? "selected" : undefined }, i18n.housingTypeRENT),
|
||||
option({ value: "couchsurfing", selected: type === "couchsurfing" ? "selected" : undefined }, i18n.housingTypeCOUCHSURFING)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingProperty),
|
||||
br(),
|
||||
select({ name: "property_type" },
|
||||
option({ value: "apartment", ...(item.property_type === "apartment" ? { selected: true } : {})}, i18n.housingPropertyAPARTMENT),
|
||||
option({ value: "house", ...(item.property_type === "house" ? { selected: true } : {})}, i18n.housingPropertyHOUSE),
|
||||
option({ value: "room", ...(item.property_type === "room" ? { selected: true } : {})}, i18n.housingPropertyROOM),
|
||||
option({ value: "land", ...(item.property_type === "land" ? { selected: true } : {})}, i18n.housingPropertyLAND),
|
||||
option({ value: "other", ...(item.property_type === "other" ? { selected: true } : {})}, i18n.housingPropertyOTHER)
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingTitleLabel),
|
||||
br(),
|
||||
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.housingTitlePlaceholder, value: item.title || "" }),
|
||||
br(),
|
||||
br(),
|
||||
...renderGalleryFields(item, isEdit, maxImages),
|
||||
label(i18n.housingDescription),
|
||||
br(),
|
||||
textarea({ name: "description", rows: "6", required: true, placeholder: i18n.housingDescriptionPlaceholder }, item.description || ""),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingRules),
|
||||
br(),
|
||||
textarea({ name: "rules", rows: "4", placeholder: i18n.housingRulesPlaceholder }, item.rules || ""),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingPlace),
|
||||
br(),
|
||||
input({ type: "text", name: "place", placeholder: i18n.housingPlacePlaceholder, value: item.place || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.mapLocationTitle || "Map Location"),
|
||||
br(),
|
||||
input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: item.mapUrl || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingPriceLabel),
|
||||
br(),
|
||||
input({ type: "number", name: "price", step: "0.01", min: "0", value: item.price || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingRooms),
|
||||
br(),
|
||||
input({ type: "number", name: "rooms", min: "0", value: item.rooms || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingSizeLabel),
|
||||
br(),
|
||||
input({ type: "number", name: "size", step: "0.01", min: "0", value: item.size || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingCapacity),
|
||||
br(),
|
||||
input({ type: "number", name: "capacity", min: "0", value: item.capacity || "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingAvailableFrom),
|
||||
br(),
|
||||
input({ type: "date", name: "availableFrom", required: true, min: isEdit ? undefined : today(), value: item.availableFrom ? String(item.availableFrom).slice(0, 10) : "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingAvailableTo),
|
||||
br(),
|
||||
input({ type: "date", name: "availableTo", min: item.availableFrom ? String(item.availableFrom).slice(0, 10) : today(), value: item.availableTo ? String(item.availableTo).slice(0, 10) : "" }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.housingTags),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", value: Array.isArray(item.tags) ? item.tags.join(", ") : (item.tags || "") }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.visibilityLabel || "Visibility"),
|
||||
br(),
|
||||
select({ name: "visibility" },
|
||||
option({ value: "PUBLIC", ...((item.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"),
|
||||
option({ value: "HIDDEN", ...(item.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden")
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
button({ type: "submit" }, isEdit ? i18n.housingUpdateButton : i18n.housingCreateButton)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const renderFiltersBar = (filter, params = {}) =>
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/housing", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "search", value: safeText(params.search || "") }),
|
||||
input({ type: "hidden", name: "minPrice", value: String(params.minPrice ?? "") }),
|
||||
input({ type: "hidden", name: "maxPrice", value: String(params.maxPrice ?? "") }),
|
||||
input({ type: "hidden", name: "place", value: safeText(params.place || "") }),
|
||||
input({ type: "hidden", name: "sort", value: safeText(params.sort || "") }),
|
||||
...FILTERS.map(f =>
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase())
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.housingCreateButton)
|
||||
)
|
||||
)
|
||||
|
||||
exports.housingView = async (items, filter = "ALL", params = {}) => {
|
||||
const search = safeText(params.search || "")
|
||||
const minPrice = params.minPrice ?? ""
|
||||
const maxPrice = params.maxPrice ?? ""
|
||||
const place = safeText(params.place || "")
|
||||
const sort = safeText(params.sort || "recent")
|
||||
|
||||
const isForm = filter === "CREATE" || filter === "EDIT"
|
||||
|
||||
return template(
|
||||
i18n.housingTitle,
|
||||
section(
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.housingTitle),
|
||||
p(i18n.housingDescriptionText)
|
||||
),
|
||||
renderFiltersBar(filter, { search, minPrice, maxPrice, place, sort })
|
||||
),
|
||||
section(
|
||||
isForm
|
||||
? renderHousingForm(filter === "EDIT" ? (Array.isArray(items) ? items[0] : items) || {} : (params.draft || {}), filter === "EDIT" ? "edit" : "create", Number(params.maxImages) > 0 ? Number(params.maxImages) : MAX_IMAGES, await renderSpreadEditWarning(filter === "EDIT" ? ((Array.isArray(items) ? items[0] : items) || {}).id : null))
|
||||
: section(
|
||||
div({ class: "housing-search" },
|
||||
form({ method: "GET", action: "/housing", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter || "ALL" }),
|
||||
input({ type: "text", name: "search", value: search, placeholder: i18n.housingSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
input({ type: "text", name: "place", value: place, placeholder: i18n.housingPlacePlaceholder, class: "filter-box__input housing-place-input" }),
|
||||
div({ class: "transfer-range" },
|
||||
input({ type: "number", name: "minPrice", step: "0.01", min: "0", value: String(minPrice ?? ""), placeholder: i18n.housingMinPrice, class: "filter-box__number transfer-amount-input" }),
|
||||
input({ type: "number", name: "maxPrice", step: "0.01", min: "0", value: String(maxPrice ?? ""), placeholder: i18n.housingMaxPrice, class: "filter-box__number transfer-amount-input" })
|
||||
),
|
||||
select({ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.housingSortRecent),
|
||||
option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.housingSortPrice),
|
||||
option({ value: "requests", ...(sort === "requests" ? { selected: true } : {})}, i18n.housingSortRequests),
|
||||
option({ value: "rating", ...(sort === "rating" ? { selected: true } : {})}, i18n.housingSortRating)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.housingSearchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
br(),
|
||||
div({ class: "housing-list" }, renderHousingList(items, filter, params))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const renderCommentsSection = (itemId, returnTo, comments = []) => {
|
||||
return renderSharedCommentsSection({
|
||||
action: `/housing/${encodeURIComponent(itemId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
exports.singleHousingView = async (item, filter = "ALL", comments = [], params = {}) => {
|
||||
const returnTo = safeText(params.returnTo) || buildReturnTo(filter, params)
|
||||
const isAuthor = String(item.author) === String(userId)
|
||||
const requested = safeArr(item.requests).includes(userId)
|
||||
const voters = safeArr(item.opinions_inhabitants)
|
||||
|
||||
const chips = [
|
||||
renderTypeChip(item),
|
||||
renderStatusChip(item.status),
|
||||
item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null,
|
||||
requested ? renderRequestedChip() : null,
|
||||
renderLifespanChip(item.lifetime, i18n),
|
||||
renderEcoTax(item.msgSize, item.id)
|
||||
].filter(Boolean)
|
||||
|
||||
const sideActions = []
|
||||
const requestToggle = renderRequestToggle(item, returnTo)
|
||||
if (requestToggle) sideActions.push(requestToggle)
|
||||
const ownerActions = renderOwnerActions(item, returnTo)
|
||||
|
||||
const nextVisibility = item.visibility === "PUBLIC" ? "HIDDEN" : "PUBLIC"
|
||||
const isHidden = item.visibility === "HIDDEN"
|
||||
const visibilityRow = isAuthor
|
||||
? div({ class: "tribe-side-actions shop-visibility-row housing-visibility-row" },
|
||||
span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `),
|
||||
isHidden
|
||||
? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E")
|
||||
: renderStateChip("mutuals", "👁", i18n.visibilityPublic || "PUBLIC"),
|
||||
form({ method: "POST", action: `/housing/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "visibility", value: nextVisibility }),
|
||||
button({ type: "submit", class: "tribe-action-btn" },
|
||||
nextVisibility === "PUBLIC" ? (i18n.visibilityMakePublic || "Make public") : (i18n.visibilityMakeHidden || "Make hidden"))
|
||||
)
|
||||
)
|
||||
: null
|
||||
|
||||
const cover = imagesOf(item)[0]
|
||||
const housingSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(item.id, null, { spread: params.spreads || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, safeText(item.title) || i18n.housingTitle)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
renderStarRating(item.opinions, voters.length),
|
||||
cover && !isVideoEntry(cover) ? img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-detail-image", alt: "" }) : null,
|
||||
renderInfoTable(item),
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`)
|
||||
),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null,
|
||||
renderStatusRow(item, returnTo),
|
||||
ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null,
|
||||
visibilityRow,
|
||||
renderTags(item.tags)
|
||||
)
|
||||
|
||||
const renderSection = (titleText, bodyText) =>
|
||||
safeText(bodyText)
|
||||
? div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, titleText),
|
||||
p({ class: "tribe-side-description" }, ...renderUrl(bodyText))
|
||||
)
|
||||
: null
|
||||
|
||||
const housingMain = div({ class: "tribe-main" },
|
||||
renderPhotoGallery(item),
|
||||
renderSection(i18n.housingDescription, item.description),
|
||||
renderSection(i18n.housingRules, item.rules),
|
||||
item.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, item.mapUrl)) : null,
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(item.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(item.author)
|
||||
),
|
||||
renderEngagement(item.id,
|
||||
!isAuthor && item.everRequestedByViewer
|
||||
? renderOpinionsVoting("/housing/opinions", item.id, item.opinions, returnTo, voters)
|
||||
: null,
|
||||
renderCommentsSection(item.id, returnTo, comments)
|
||||
)
|
||||
)
|
||||
|
||||
return template(
|
||||
i18n.housingTitle,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(i18n.housingTitle), p(i18n.housingDescriptionText)),
|
||||
renderFiltersBar(filter, params),
|
||||
div({ class: "tribe-details" }, housingSide, housingMain)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -1186,6 +1186,8 @@ const renderBottomBar = forkNav.renderBottomBar;
|
|||
const renderHiveNav = forkNav.renderHiveNav;
|
||||
exports.PINNABLE_MODULES = forkNav.PINNABLE_MODULES;
|
||||
exports.renderHiveNav = renderHiveNav;
|
||||
// se resuelve al llamarse, cuando template ya esta definido mas abajo
|
||||
exports.bottomBarPickerView = () => forkNav.bottomBarPickerView(template);
|
||||
|
||||
const template = (titlePrefix, ...elements) => {
|
||||
const currentConfig = getConfig();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue