escala 2+3: Meteor 1.8.3 -> 2.3 (Mongo 3.2 compatible), dead-package remediation
Reaches Meteor 2.3 building + running against Mongo 3.2, REST smoke test
byte-identical to the 1.6.1.1 baseline (all 12 Flutter endpoints green).
Dead/abandoned Atmosphere packages removed or replaced (the upgrade blockers):
- arkham:comments-ui (no Meteor 2.x build; pinned accounts-password@1.x):
reimplemented the fire-page comments as a React feature:
imports/api/Comments/ (collection, server methods, publication, media
analyzers, new-fire-comment email) + imports/ui/components/Comments/CommentsBox.
Comment text now rendered as safe plain text + image/youtube embed (was
markdown-to-HTML). Wired into Fires.js; sitemaps.js + migration v11 updated
to the new collection. Old Blaze/startup comments files deleted.
- nimble:restivus (REST API; pinned accounts-password@1.3.3, CoffeeScript source
that crashes this build host): vendored as a local package
packages/nimble-restivus with the .coffee precompiled to plain JS and the
accounts-password constraint loosened to 2.x. REST behavior unchanged
(verified by smoke test). This also dropped the entire iron:router stack.
- maximum:server-transform (+ peerlibrary:*, meteorhacks:zones/inject-initial):
unused; removal broke Meteor.publishTransformed in FalsePositives publications
-> replaced with plain Meteor.publish (no transform was configured).
- less, markdown (no source files), and the dead test stack
(meteortesting:mocha, practicalmeteor:chai, xolvio:cleaner) which transitively
pulled coffeescript@1.0.17 — the build plugin that crashed meteor-tool
(node_contextify assertion) on this machine. Removing it unblocked the build.
- fourseven:scss 4.5.4 -> 4.14.1 (node-sass 4.5.3 doesn't build on node 12).
npm-mongo driver at 2.3 is 3.9.x — still compatible with the production Mongo
3.2 replica set.
This commit is contained in:
parent
ccfc80547a
commit
cd570b9d9c
24 changed files with 1638 additions and 312 deletions
76
imports/ui/components/Comments/Comments.scss
Normal file
76
imports/ui/components/Comments/Comments.scss
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
.comments-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comments-box {
|
||||
max-width: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
padding: 0.75em 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
p.comment-content {
|
||||
white-space: pre-line;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.comment-media-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.comment-media-youtube iframe {
|
||||
width: 100%;
|
||||
min-height: 240px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.comment-actions .btn {
|
||||
padding-left: 0;
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
.comment-actions .btn.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* keep the previous "remove" button neutral (not danger red) */
|
||||
.comment-owner-actions .remove-action {
|
||||
color: rgb(51, 51, 51);
|
||||
}
|
||||
|
||||
.comment-form .create-comment {
|
||||
height: 9em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.comment-login-hint {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.img-avatar {
|
||||
margin-right: 5px;
|
||||
}
|
||||
152
imports/ui/components/Comments/CommentsBox.js
Normal file
152
imports/ui/components/Comments/CommentsBox.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/* eslint-disable import/no-absolute-path */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import moment from 'moment';
|
||||
import CommentsCollection from '/imports/api/Comments/Comments';
|
||||
import './Comments.scss';
|
||||
|
||||
function MediaEmbed({ media }) {
|
||||
if (!media || !media.content) return null;
|
||||
if (media.type === 'image') {
|
||||
return <img className="comment-media-image" src={media.content} alt="" />;
|
||||
}
|
||||
if (media.type === 'youtube') {
|
||||
return (
|
||||
<div className="comment-media-youtube">
|
||||
<iframe title="youtube" src={media.content} frameBorder="0" allowFullScreen />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
MediaEmbed.propTypes = { media: PropTypes.object };
|
||||
MediaEmbed.defaultProps = { media: null };
|
||||
|
||||
class CommentsBox extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { draft: '', editingId: null, editDraft: '' };
|
||||
}
|
||||
|
||||
call(method, ...args) {
|
||||
Meteor.call(method, ...args, (err) => {
|
||||
if (err) console.warn(`${method}: ${err.reason || err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
submit = (e) => {
|
||||
e.preventDefault();
|
||||
const content = this.state.draft.trim();
|
||||
if (!content) return;
|
||||
this.setState({ draft: '' });
|
||||
this.call('comments.insert', this.props.referenceId, content);
|
||||
};
|
||||
|
||||
saveEdit = (id) => {
|
||||
const content = this.state.editDraft.trim();
|
||||
if (!content) return;
|
||||
this.setState({ editingId: null, editDraft: '' });
|
||||
this.call('comments.edit', id, content);
|
||||
};
|
||||
|
||||
renderComment(c) {
|
||||
const { t } = this.props;
|
||||
const currentUserId = Meteor.userId();
|
||||
const isOwner = currentUserId && c.userId === currentUserId;
|
||||
const likes = (c.likes || []).length;
|
||||
const dislikes = (c.dislikes || []).length;
|
||||
const liked = currentUserId && (c.likes || []).includes(currentUserId);
|
||||
const disliked = currentUserId && (c.dislikes || []).includes(currentUserId);
|
||||
const author = c.username || t('Anónimo');
|
||||
return (
|
||||
<li key={c._id} className="comment">
|
||||
<div className="comment-header">
|
||||
<span className="comment-author">{author}</span>
|
||||
<span className="comment-date">{moment(c.createdAt).format('LLL')}</span>
|
||||
</div>
|
||||
{this.state.editingId === c._id ? (
|
||||
<div className="comment-edit">
|
||||
<textarea
|
||||
className="form-control"
|
||||
value={this.state.editDraft}
|
||||
onChange={ev => this.setState({ editDraft: ev.target.value })}
|
||||
/>
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={() => this.saveEdit(c._id)}>{t('Guardar')}</button>
|
||||
<button type="button" className="btn btn-link btn-sm" onClick={() => this.setState({ editingId: null })}>{t('Borrar')}</button>
|
||||
</div>
|
||||
) : (
|
||||
// Plain text (whitespace preserved via CSS). Rendered as text — not
|
||||
// HTML — so user content can never inject markup (safer than the old
|
||||
// markdown-to-HTML comments package).
|
||||
<p className="comment-content">{c.content}</p>
|
||||
)}
|
||||
<MediaEmbed media={c.media} />
|
||||
<div className="comment-actions">
|
||||
<button type="button" className={`btn btn-link btn-sm${liked ? ' active' : ''}`} disabled={!currentUserId} onClick={() => this.call('comments.like', c._id)}>
|
||||
{'👍'} {likes}
|
||||
</button>
|
||||
<button type="button" className={`btn btn-link btn-sm${disliked ? ' active' : ''}`} disabled={!currentUserId} onClick={() => this.call('comments.dislike', c._id)}>
|
||||
{'👎'} {dislikes}
|
||||
</button>
|
||||
{isOwner && (
|
||||
<span className="comment-owner-actions">
|
||||
<button type="button" className="btn btn-link btn-sm" onClick={() => this.setState({ editingId: c._id, editDraft: c.content })}>{t('Editar')}</button>
|
||||
<button type="button" className="btn btn-link btn-sm remove-action" onClick={() => this.call('comments.remove', c._id)}>{t('Borrar')}</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, comments, ready } = this.props;
|
||||
const loggedIn = !!Meteor.userId();
|
||||
return (
|
||||
<div className="comments-box">
|
||||
<ul className="comments-list">
|
||||
{comments.map(c => this.renderComment(c))}
|
||||
</ul>
|
||||
{ready && comments.length === 0 && (
|
||||
<p className="comments-empty">{t('Añadir un comentario')}</p>
|
||||
)}
|
||||
{loggedIn ? (
|
||||
<form className="comment-form" onSubmit={this.submit}>
|
||||
<textarea
|
||||
className="form-control create-comment"
|
||||
placeholder={t('Añadir un comentario')}
|
||||
value={this.state.draft}
|
||||
onChange={ev => this.setState({ draft: ev.target.value })}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">{t('Añadir comentario')}</button>
|
||||
</form>
|
||||
) : (
|
||||
<p className="comment-login-hint">
|
||||
{t('Necesitas iniciar sesión para')} {t('añadir comentarios')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentsBox.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
referenceId: PropTypes.string.isRequired,
|
||||
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
ready: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const CommentsBoxContainer = withTracker(({ referenceId }) => {
|
||||
const handle = Meteor.subscribe('comments.forReference', referenceId);
|
||||
return {
|
||||
ready: handle.ready(),
|
||||
comments: CommentsCollection.find({ referenceId }, { sort: { createdAt: 1 } }).fetch()
|
||||
};
|
||||
})(CommentsBox);
|
||||
|
||||
export default translate([], { wait: true })(CommentsBoxContainer);
|
||||
Loading…
Add table
Add a link
Reference in a new issue