Modernizes the whole i18n stack:
- 48 translate([], {wait:true}) / translate() HOCs -> withTranslation()
- react.wait:true -> react.useSuspense:false
- whitelist -> supportedLngs; added compatibilityJSON: 'v3' so our
natural-language keys + v3 _plural layout keep working (custom
separators ss/eth/dj preserved)
- backends: xhr -> i18next-http-backend (client), sync-fs ->
i18next-fs-backend (server); dropped i18next-localstorage-cache
(was enabled:false); languagedetector 2 -> 8
- ReSendEmail: <Interpolate> (removed in v10) -> <Trans values={{email}}/>,
dropped the removed bare t export
- saveMissing handler signature (lngs, ns, key, fallbackValue)
Silences the per-component Translate/I18n legacy-context warnings.
Browser-verified es/en switch and <Trans> interpolation on /fires;
REST smoke byte-identical.
152 lines
5.7 KiB
JavaScript
152 lines
5.7 KiB
JavaScript
/* 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 { withTranslation } 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 withTranslation()(CommentsBoxContainer);
|