[deploy] Home Page Feed redesign + top comments (#7579)
* feed cards * frontend * preactify * preactify * . * fix * fixing things and adding dropdown * whoops. * whoops 2. * revert body bg color change * search snippet * search snippet * get rid of featured article * get rid of featured article... * cover to its own component * videos * videos * reading time * small-save-filled asset * author adjustments * adjustments * Author --> Meta * whoops. * loader * logged out state * . * dropping what's not needed for now * better name for cover * get rid of nav overflow * bringin comments placeholder back * bringing comments placeholder back * publish date * fix * date * . * save button icon * ghost button * lets skip the icon for saving for now * more buttons * counting logic * Display top comments on homepage feed * Comment list and item components * Remove 'Top Comments' title * Remove subscribe button, add reading time * Update snapshots and tests * Fix article and follower scrolling * Added reading time * (Try) to do flare tags * Button component from Pawel * More button styles from Pawel * Comment count style from Pawel * Handle empty parens for age on older articles * Dont show 'more comment' button for more than 2 comments * cover ratio * flare tag * cover fix * fix buttons * more button fixes * tags fix * responsiveness fix * reading time + a bit of responsiveness * Reading time in more places * snapshots * default color for flare tag * Update CommentsList test snapshot with new styles * fixing search results * fixing buttons * Fix podcast card tests by updating snapshots * Line clamping for top comments * Reflect save button state after click before mouseout * Now the entire feed card is clickable. (#7638) * Now pointer cursor is only on feed posts. (#7727) * Revert podcast card styling * Update test snapshot * merge * Fix rspec on feed page * Reduce complexity of class check for feed card clicking * PR feedback/fixes * Fix featured article test and snapshot * Last minute test tweaks * clean up crayons * padding issue + empty div * Correct merge error * Fix search results tests * Test fixes and snapshot updates * themes * podcasts colors Co-authored-by: ludwiczakpawel <ludwiczakpawel@gmail.com> Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
This commit is contained in:
parent
7cd38e2c46
commit
2abe830377
80 changed files with 4066 additions and 2871 deletions
1
app/assets/images/overflow-horizontal.svg
Normal file
1
app/assets/images/overflow-horizontal.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 12a2 2 0 11-4 0 2 2 0 014 0zm7 0a2 2 0 11-4 0 2 2 0 014 0zm5 2a2 2 0 100-4 2 2 0 000 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 212 B |
1
app/assets/images/small-comment.svg
Normal file
1
app/assets/images/small-comment.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 5h3a6 6 0 110 12v2.625c-3.75-1.5-9-3.75-9-8.625a6 6 0 016-6zM12 15.5h1.5a4.501 4.501 0 001.722-8.657A4.5 4.5 0 0013.5 6.5h-3A4.5 4.5 0 006 11c0 2.707 1.846 4.475 6 6.36V15.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
1
app/assets/images/small-heart.svg
Normal file
1
app/assets/images/small-heart.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M18.884 12.595l.01.011L12 19.5l-6.894-6.894.01-.01A4.875 4.875 0 0112 5.73a4.875 4.875 0 016.884 6.865zM6.431 7.037a3.375 3.375 0 000 4.773L12 17.38l5.569-5.569a3.375 3.375 0 10-4.773-4.773L9.613 10.22l-1.06-1.062 2.371-2.372a3.375 3.375 0 00-4.492.25v.001z"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
1
app/assets/images/small-save-filled.svg
Normal file
1
app/assets/images/small-save-filled.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M6.75 4.5h10.5a.75.75 0 01.75.75v14.357a.375.375 0 01-.575.318L12 16.523l-5.426 3.401A.375.375 0 016 19.607V5.25a.75.75 0 01.75-.75z"/></svg>
|
||||
|
After Width: | Height: | Size: 214 B |
1
app/assets/images/small-save.svg
Normal file
1
app/assets/images/small-save.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M6.75 4.5h10.5a.75.75 0 01.75.75v14.357a.375.375 0 01-.575.318L12 16.523l-5.426 3.401A.375.375 0 016 19.607V5.25a.75.75 0 01.75-.75zM16.5 6h-9v11.574l4.5-2.82 4.5 2.82V6z" /></svg>
|
||||
|
After Width: | Height: | Size: 253 B |
|
|
@ -44,7 +44,7 @@ function insertNext(params, buildCallback) {
|
|||
var distanceFromBottom = document.documentElement.scrollHeight - document.body.scrollTop;
|
||||
var newNode = document.createElement("div");
|
||||
newNode.innerHTML = newFollowersHTML;
|
||||
var singleArticles = document.getElementsByClassName("single-article");
|
||||
var singleArticles = document.querySelectorAll(".single-article, .crayons-story");
|
||||
var lastElement = singleArticles[singleArticles.length - 1];
|
||||
insertAfter(newNode, lastElement);
|
||||
if (nextPage > 0) {
|
||||
|
|
@ -133,7 +133,7 @@ function insertVideos(videoArticles) {
|
|||
var newNode = document.createElement("div");
|
||||
newNode.innerHTML = newVideosHTML;
|
||||
newNode.className += "video-collection"
|
||||
var singleArticles = document.getElementsByClassName("single-article");
|
||||
var singleArticles = document.querySelectorAll(".single-article, .crayons-story");
|
||||
var lastElement = singleArticles[singleArticles.length - 1];
|
||||
insertAfter(newNode, lastElement);
|
||||
if (nextPage > 0) {
|
||||
|
|
@ -155,7 +155,7 @@ function insertArticles(articles) {
|
|||
if ( !['/', '/top/week', '/top/month', '/top/year', '/top/infinity', '/latest'].includes(window.location.pathname) &&
|
||||
existingEl &&
|
||||
existingEl.parentElement &&
|
||||
existingEl.parentElement.classList.contains('single-article-small-pic') &&
|
||||
existingEl.parentElement.classList.contains('crayons-story') &&
|
||||
!document.getElementById("video-player-"+article.id)) {
|
||||
existingEl.parentElement.outerHTML = buildArticleHTML(article);
|
||||
} else if(!existingEl) {
|
||||
|
|
@ -166,8 +166,37 @@ function insertArticles(articles) {
|
|||
});
|
||||
var distanceFromBottom = document.documentElement.scrollHeight - document.body.scrollTop;
|
||||
var newNode = document.createElement("div");
|
||||
newNode.classList.add('paged-stories');
|
||||
newNode.innerHTML = newArticlesHTML;
|
||||
var singleArticles = document.getElementsByClassName("single-article");
|
||||
|
||||
newNode.addEventListener('click', (event) => {
|
||||
const { classList } = event.target;
|
||||
|
||||
// This looks a little messy, but it's the only
|
||||
// way to make the entire card clickable.
|
||||
if (
|
||||
classList.contains('crayons-story') ||
|
||||
classList.contains('crayons-story__top') ||
|
||||
classList.contains('crayons-story__body') ||
|
||||
classList.contains('crayons-story__indention') ||
|
||||
classList.contains('crayons-story__title') ||
|
||||
classList.contains('crayons-story__tags') ||
|
||||
classList.contains('crayons-story__bottom')
|
||||
) {
|
||||
let element = event.target;
|
||||
let { articlePath } = element.dataset;
|
||||
|
||||
while (!articlePath) {
|
||||
articlePath = element.dataset.articlePath;
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
InstantClick.preload(articlePath);
|
||||
InstantClick.display(articlePath);
|
||||
}
|
||||
})
|
||||
|
||||
var singleArticles = document.querySelectorAll(".single-article, .crayons-story");
|
||||
var lastElement = singleArticles[singleArticles.length - 1];
|
||||
insertAfter(newNode, lastElement);
|
||||
if (nextPage > 0) {
|
||||
|
|
@ -287,7 +316,8 @@ function fetchNextPageIfNearBottom() {
|
|||
}
|
||||
|
||||
function checkIfNearBottomOfPage() {
|
||||
if (document.getElementsByClassName("single-article").length < 2 || window.location.search.indexOf("q=") > -1 ) {
|
||||
if ((document.getElementsByClassName("crayons-story").length < 2 && document.getElementsByClassName("single-article").length < 2)
|
||||
|| window.location.search.indexOf("q=") > -1 ) {
|
||||
document.getElementById("loading-articles").style.display = "none";
|
||||
done = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
/* Show article date/time according to user's locale */
|
||||
/* global addLocalizedDateTimeToElementsTitles */
|
||||
|
||||
function initializeArticleDate() {
|
||||
var articlesDates = document.querySelectorAll(
|
||||
'.single-article time, article time, .single-other-article time',
|
||||
'.crayons-story time, article time, .single-other-article time',
|
||||
);
|
||||
|
||||
addLocalizedDateTimeToElementsTitles(articlesDates, 'datetime');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
/**
|
||||
* This script hunts for podcast's "Record" for both the podcast_episde's
|
||||
|
|
|
|||
|
|
@ -131,11 +131,11 @@ function addHoverEffectToReadingListButtons() {
|
|||
// `this` local variable
|
||||
container.addEventListener(
|
||||
'mouseover',
|
||||
readingListButtonMouseHandler.bind('UNSAVE'),
|
||||
readingListButtonMouseHandler.bind('Unsave'),
|
||||
);
|
||||
container.addEventListener(
|
||||
'mouseout',
|
||||
readingListButtonMouseHandler.bind('SAVED'),
|
||||
readingListButtonMouseHandler.bind('Saved'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,34 +20,45 @@ function buildArticleHTML(article) {
|
|||
var tagList = article.tag_list || article.cached_tag_list_array
|
||||
if(tagList) {
|
||||
tagList.forEach(function(t){
|
||||
tagString = tagString + '<a href="/t/'+t+'"><span class="tag">#'+t+'</span></a>\n'
|
||||
tagString = tagString + '<a href="/t/'+t+'" class="crayons-tag"><span class="crayons-tag__prefix">#</span>'+t+'</a>\n'
|
||||
});
|
||||
}
|
||||
var commentsCountHTML = ""
|
||||
if ((article.comments_count || '0') > 0 && article.class_name != "User") {
|
||||
commentsCountHTML = '<div class="article-engagement-count comments-count"><a href="'+article.path+'#comments"><img src="<%= asset_path("comments-bubble.png") %>" alt="chat" /><span class="engagement-count-number">'+(article.comments_count || '0')+'</span></a></div>'
|
||||
|
||||
var commentsDisplay = ''
|
||||
var commentsCount = '0'
|
||||
|
||||
if ((article.comments_count || '0') > 0) {
|
||||
var commentsCount = (article.comments_count || '0')
|
||||
}
|
||||
if (article.class_name != "User") {
|
||||
var commentsDisplay = '<a href="'+article.path+'#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left "><svg class="crayons-icon" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 5h3a6 6 0 110 12v2.625c-3.75-1.5-9-3.75-9-8.625a6 6 0 016-6zM12 15.5h1.5a4.501 4.501 0 001.722-8.657A4.5 4.5 0 0013.5 6.5h-3A4.5 4.5 0 006 11c0 2.707 1.846 4.475 6 6.36V15.5z"/></svg>'+commentsCount+'<span class="hidden s:inline"> comments</span></a>'
|
||||
}
|
||||
|
||||
var flareTag = ""
|
||||
if (container){
|
||||
var currentTag = JSON.parse(container.dataset.params).tag
|
||||
}
|
||||
if (article.flare_tag && currentTag != article.flare_tag.name) {
|
||||
flareTag = "<span class='tag-identifier' style='background:"+article.flare_tag.bg_color_hex+";color:"+article.flare_tag.text_color_hex+"'>#"+article.flare_tag.name+"</span>"
|
||||
flareTag = "<span class='crayons-story__flare-tag' style='background:"+article.flare_tag.bg_color_hex+";color:"+article.flare_tag.text_color_hex+"'>#"+article.flare_tag.name+"</span>"
|
||||
}
|
||||
if (article.class_name == "PodcastEpisode"){
|
||||
flareTag = "<span class='tag-identifier'>podcast</span>"
|
||||
flareTag = "<span class='crayons-story__flare-tag'>podcast</span>"
|
||||
}
|
||||
if (article.class_name == "Comment"){
|
||||
flareTag = "<span class='tag-identifier'>comment</span>"
|
||||
flareTag = "<span class='crayons-story__flare-tag'>comment</span>"
|
||||
}
|
||||
if (article.class_name == "User"){
|
||||
flareTag = "<span class='tag-identifier' style='background:#5874d9;color:white;'>person</span>"
|
||||
flareTag = "<span class='crayons-story__flare-tag' style='background:#5874d9;color:white;'>person</span>"
|
||||
}
|
||||
|
||||
var rc = article.positive_reactions_count
|
||||
var reactionsCountHTML = ''
|
||||
if ((rc || '0') > 0 && article.class_name != "User") {
|
||||
var reactionsCountHTML = '<div class="article-engagement-count reactions-count"><a href="'+article.path+'"><img src="<%= asset_path("reactions-stack.png") %>" alt="heart" /><span id="engagement-count-number-'+article.id+'" class="engagement-count-number">'+(rc || '0')+'</span></a></div>'
|
||||
var reactionsCount = (rc || '0')
|
||||
var reactionsDisplay = '';
|
||||
|
||||
if (article.class_name != "User") {
|
||||
reactionsDisplay = '<a href="'+article.path+'" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"><svg class="crayons-icon" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M18.884 12.595l.01.011L12 19.5l-6.894-6.894.01-.01A4.875 4.875 0 0112 5.73a4.875 4.875 0 016.884 6.865zM6.431 7.037a3.375 3.375 0 000 4.773L12 17.38l5.569-5.569a3.375 3.375 0 10-4.773-4.773L9.613 10.22l-1.06-1.062 2.371-2.372a3.375 3.375 0 00-4.492.25v.001z"/></svg>'+reactionsCount+'<span class="hidden s:inline"> reactions</span></a>'
|
||||
}
|
||||
|
||||
var picUrl;
|
||||
var profileUsername;
|
||||
var userName;
|
||||
|
|
@ -61,9 +72,49 @@ function buildArticleHTML(article) {
|
|||
userName = article.user.name;
|
||||
}
|
||||
var orgHeadline = "";
|
||||
var forOrganization = "";
|
||||
var organizationLogo = "";
|
||||
var organizationClasses = "crayons-avatar--l";
|
||||
|
||||
|
||||
if (article.organization && !document.getElementById("organization-article-index")) {
|
||||
orgHeadline = '<div class="article-organization-headline"><a class="org-headline-filler" href="/'+article.organization.slug+'"><img alt="'+article.organization.name+' logo" src="'+article.organization.profile_image_90+'" loading="lazy">'+article.organization.name+'</a></div>'
|
||||
var organizationLogo = '<a href="/'+article.organization.slug+'" class="crayons-logo crayons-logo--l"><img alt="'+article.organization.name+' logo" src="'+article.organization.profile_image_90+'" class="crayons-logo__image" loading="lazy"/></a>';
|
||||
var forOrganization = '<span><span class="crayons-story__tertiary fw-normal"> for </span><a href="/'+article.organization.slug+'" class="crayons-story__secondary fw-medium">'+article.organization.name+'</a></span>';
|
||||
var organizationClasses = "crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted";
|
||||
}
|
||||
|
||||
var timeAgoInWords = '';
|
||||
if (article.published_at_int) {
|
||||
timeAgoInWords = timeAgo({ oldTimeInSeconds: article.published_at_int });
|
||||
}
|
||||
|
||||
var publishDate = '';
|
||||
if (article.readable_publish_date) {
|
||||
if (article.published_timestamp) {
|
||||
publishDate = '<time datetime="'+article.published_timestamp+'">'+article.readable_publish_date+' '+timeAgoInWords+'</time>';
|
||||
} else {
|
||||
publishDate = '<time>'+article.readable_publish_date+' '+timeAgoInWords+'</time>';
|
||||
}
|
||||
}
|
||||
|
||||
var meta = '<div class="crayons-story__meta">\
|
||||
<div class="crayons-story__author-pic">\
|
||||
'+organizationLogo+'\
|
||||
<a href="/'+profileUsername+'" class="crayons-avatar '+organizationClasses+'">\
|
||||
<img src="'+picUrl+'" alt="'+profileUsername+' profile" class="crayons-avatar__image" loading="lazy" />\
|
||||
</a>\
|
||||
</div>\
|
||||
<div>\
|
||||
<p>\
|
||||
<a href="/'+profileUsername+'" class="crayons-story__secondary fw-medium">'+filterXSS(article.user.name)+'</a>\
|
||||
'+forOrganization+'\
|
||||
</p>\
|
||||
<a href="'+article.path+'" class="crayons-story__tertiary fs-xs">\
|
||||
'+publishDate+'\
|
||||
</a>\
|
||||
</div>\
|
||||
</div>';
|
||||
|
||||
var bodyTextSnippet = "";
|
||||
var searchSnippetHTML = "";
|
||||
if (article.highlight && article.highlight.body_text.length > 0){
|
||||
|
|
@ -74,64 +125,55 @@ function buildArticleHTML(article) {
|
|||
}
|
||||
bodyTextSnippet = startingEllipsis + article.highlight.body_text.join("...") + "…";
|
||||
if (bodyTextSnippet.length > 0){
|
||||
searchSnippetHTML = '<div class="search-snippet"><span>' + bodyTextSnippet + '</span></div>'
|
||||
searchSnippetHTML = '<div class="crayons-story__snippet mb-1">' + bodyTextSnippet + '</div>'
|
||||
}
|
||||
}
|
||||
|
||||
var readingTimeHTML = '';
|
||||
if (article.class_name === "Article") {
|
||||
// we have ` ... || null` for the case article.reading_time is undefined
|
||||
readingTimeHTML = '<small class="crayons-story__tertiary fs-xs mr-2">'+ ((article.reading_time || null) < 1 ? '1 min' : article.reading_time + ' min') +' read</small>'
|
||||
}
|
||||
|
||||
var saveButton = '';
|
||||
if (article.class_name === "Article") {
|
||||
saveButton = '<button type="button" class="article-engagement-count engage-button bookmark-button" data-reactable-id="'+article.id+'">\
|
||||
<span class="bm-initial">SAVE</span>\
|
||||
<span class="bm-success">SAVED</span>\
|
||||
saveButton = '<button type="button" class="crayons-btn crayons-btn--secondary crayons-btn--s bookmark-button" data-reactable-id="'+article.id+'">\
|
||||
<span class="bm-initial">Save</span>\
|
||||
<span class="bm-success">Saved</span>\
|
||||
</button>'
|
||||
} else if (article.class_name === "User") {
|
||||
saveButton = '<button type="button" style="width: 122px" class="article-engagement-count engage-button follow-action-button follow-user"\
|
||||
saveButton = '<button type="button" class="crayons-btn crayons-btn--secondary crayons-btn--icon-left fs-s bookmark-button article-engagement-count engage-button follow-action-button follow-user"\
|
||||
data-info=\'{"id":'+article.id+',"className":"User"}\' data-follow-action-button>\
|
||||
\
|
||||
</button>'
|
||||
}
|
||||
|
||||
var publishDate = '';
|
||||
if (article.readable_publish_date) {
|
||||
if (article.published_timestamp) {
|
||||
publishDate = '・' + '<time datetime="'+article.published_timestamp+'">'+article.readable_publish_date+'</time>';
|
||||
} else {
|
||||
publishDate = '・' + '<time>'+article.readable_publish_date+'</time>';
|
||||
}
|
||||
}
|
||||
|
||||
var readingTimeHTML = '';
|
||||
if (article.class_name === "Article") {
|
||||
// we have ` ... || null` for the case article.reading_time is undefined
|
||||
readingTimeHTML = '<a href="'+article.path+'" class="article-reading-time">'+ ((article.reading_time || null) < 1 ? '1 min' : article.reading_time + ' min') +' read</a>'
|
||||
}
|
||||
|
||||
var videoHTML = '';
|
||||
if (article.cloudinary_video_url) {
|
||||
videoHTML = '<a href="'+article.path+'" class="single-article-video-preview" style="background-image:url('+article.cloudinary_video_url+')"><div class="single-article-video-duration"><img src="<%= asset_path("video-camera.svg") %>" alt="video camera">'+(article.video_duration_string || article.video_duration_in_minutes)+'</div></a>'
|
||||
videoHTML = '<a href="'+article.path+'" class="crayons-story__video" style="background-image:url('+article.cloudinary_video_url+')"><div class="crayons-story__video__time"><img src="<%= asset_path("video-camera.svg") %>" alt="video camera">'+(article.video_duration_string || article.video_duration_in_minutes)+'</div></a>'
|
||||
}
|
||||
|
||||
var timeAgoInWords = '';
|
||||
if (article.published_at_int) {
|
||||
timeAgoInWords = timeAgo({ oldTimeInSeconds: article.published_at_int });
|
||||
}
|
||||
|
||||
return '<div class="single-article single-article-small-pic" data-content-user-id="'+article.user_id+'">\
|
||||
'+videoHTML+'\
|
||||
'+orgHeadline+'\
|
||||
<div class="small-pic">\
|
||||
<a href="/'+profileUsername+'" class="small-pic-link-wrapper">\
|
||||
<img src="'+picUrl+'" alt="'+profileUsername+' profile" loading="lazy">\
|
||||
</a>\
|
||||
</div>\
|
||||
<a href="'+article.path+'" class="small-pic-link-wrapper index-article-link" id="article-link-'+article.id+'">\
|
||||
<div class="content">\
|
||||
<h3>'+flareTag+filterXSS(article.title)+'</h3>\
|
||||
'+searchSnippetHTML+'\
|
||||
return '<article class="crayons-story" data-article-path="' + article.path + '" data-content-user-id="'+article.user_id+'">\
|
||||
<div role="presentation">\
|
||||
'+videoHTML+'\
|
||||
<div class="crayons-story__body">\
|
||||
<div class="crayons-story__top">\
|
||||
'+meta+'\
|
||||
</div>\
|
||||
<div class="crayons-story__indention">\
|
||||
<h2 class="crayons-story__title"><a href="'+article.path+'" id="article-link-'+article.id+'">'+flareTag+filterXSS(article.title)+'</a></h2>\
|
||||
<div class="crayons-story__tags">'+tagString+'</div>\
|
||||
'+searchSnippetHTML+'\
|
||||
<div class="crayons-story__bottom">\
|
||||
<div class="crayons-story__details">'+reactionsDisplay+commentsDisplay+'</div>\
|
||||
<div class="crayons-story__save">\
|
||||
'+readingTimeHTML+'\
|
||||
'+saveButton+'\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
</a>\
|
||||
<h4><a href="/'+profileUsername+'">'+filterXSS(userName)+publishDate+timeAgoInWords+'</a></h4>\
|
||||
<div class="tags">'+tagString+'</div>\
|
||||
'+commentsCountHTML+reactionsCountHTML+readingTimeHTML+'\
|
||||
'+saveButton+'</div>';
|
||||
</div>\
|
||||
</article>';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@
|
|||
@include themeable(box-shadow, theme-container-box-shadow, $bold-shadow);
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--su-2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@import 'variables';
|
||||
@import '_mixins';
|
||||
@import 'config/import';
|
||||
|
||||
.home {
|
||||
position: relative;
|
||||
|
|
@ -355,34 +356,6 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
// More loading // TODO
|
||||
.single-article-loading {
|
||||
border-radius: 3px;
|
||||
animation-duration: 1.25s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: placeHolderShimmer;
|
||||
animation-timing-function: linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0.1) 10%,
|
||||
rgba(0, 0, 0, 0.05) 25%,
|
||||
rgba(255, 255, 255, 0.1) 40%
|
||||
);
|
||||
box-shadow: inset 0 0 0 200px rgba(0, 0, 0, 0.025);
|
||||
background-size: 800px 104px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes placeHolderShimmer {
|
||||
0% {
|
||||
background-position: -468px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 468px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Generally Feed...
|
||||
.articles-list {
|
||||
margin: auto;
|
||||
|
|
@ -415,10 +388,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--card-color);
|
||||
}
|
||||
|
||||
.placeholder-div {
|
||||
// TOOD: what is this?
|
||||
display: block;
|
||||
|
|
@ -562,11 +531,12 @@
|
|||
padding: 16px 16px 100px 16px; // TODO: my, oh my...
|
||||
h3 {
|
||||
margin: 0px;
|
||||
font-size: 20px;
|
||||
font-size: var(--fs-xl);
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
padding: 0px;
|
||||
word-break: break-word;
|
||||
color: var(--card-color);
|
||||
.tag-identifier {
|
||||
font-size: 14px;
|
||||
background: $blue;
|
||||
|
|
@ -582,11 +552,18 @@
|
|||
background: $black;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 450px) {
|
||||
@media (min-width: $breakpoint-s) {
|
||||
h3 {
|
||||
font-size: 27px;
|
||||
line-height: 32px;
|
||||
font-size: var(--fs-2xl);
|
||||
.tag-identifier {
|
||||
font-size: 15px;
|
||||
padding: 2px 6px 3px;
|
||||
|
|
@ -655,21 +632,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.search-snippet {
|
||||
font-size: 15px;
|
||||
padding: 6px 0px 2px;
|
||||
color: lighten($dark-gray, 8%);
|
||||
line-height: 21px;
|
||||
span {
|
||||
background: darken($light-gray, 2%);
|
||||
padding: 1px;
|
||||
}
|
||||
em {
|
||||
font-weight: 600;
|
||||
color: darken($dark-gray, 6%);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px auto;
|
||||
line-height: 20px;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
border-radius: $radius;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: bottom; // todo: do we need it?
|
||||
}
|
||||
|
||||
|
|
|
|||
35
app/assets/stylesheets/components/comments.scss
Normal file
35
app/assets/stylesheets/components/comments.scss
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
@import '../config/import';
|
||||
|
||||
.crayons-comment {
|
||||
font-size: var(--fs-s);
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: calc(var(--su-1) * -1);
|
||||
padding-left: var(--su-7);
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline; //todo: default for all in-content links
|
||||
}
|
||||
|
||||
p {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: var(--su-4);
|
||||
}
|
||||
}
|
||||
34
app/assets/stylesheets/components/loaders.scss
Normal file
34
app/assets/stylesheets/components/loaders.scss
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@import '../config/import';
|
||||
|
||||
.crayons-scaffold {
|
||||
border-radius: var(--radius);
|
||||
background: var(--body-color);
|
||||
opacity: 0.025;
|
||||
}
|
||||
|
||||
.crayons-scaffold-loading {
|
||||
border-radius: var(--radius);
|
||||
animation-duration: 1.25s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: placeHolderShimmer;
|
||||
animation-timing-function: linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0.1) 10%,
|
||||
rgba(0, 0, 0, 0.05) 25%,
|
||||
rgba(255, 255, 255, 0.1) 40%
|
||||
);
|
||||
box-shadow: inset 0 0 0 200px rgba(0, 0, 0, 0.025);
|
||||
background-size: 800px 104px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes placeHolderShimmer {
|
||||
0% {
|
||||
background-position: -468px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 468px 0;
|
||||
}
|
||||
}
|
||||
303
app/assets/stylesheets/components/stories.scss
Normal file
303
app/assets/stylesheets/components/stories.scss
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
@import '../variables';
|
||||
@import '../config/import';
|
||||
|
||||
.crayons-story {
|
||||
background: var(--card-bg);
|
||||
font-size: var(--fs-base); // Todo: remove when ready.
|
||||
box-shadow: $bold-shadow, 0 0 0 1px var(--story-border); // we will kill it at some point, don't worry. just not yet.
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(
|
||||
--su-2
|
||||
); // This is basically spacing between each feed card.
|
||||
}
|
||||
|
||||
--story-padding: var(--su-3) var(--su-2) var(--su-2);
|
||||
--title-font-size: var(--fs-xl);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
--story-padding: var(--su-4);
|
||||
--title-font-size: var(--fs-2xl);
|
||||
|
||||
&--featured {
|
||||
--title-font-size: var(--fs-3xl);
|
||||
}
|
||||
}
|
||||
|
||||
// Defining additional colors.
|
||||
&__secondary {
|
||||
color: var(--card-color-secondary);
|
||||
|
||||
&[href]:hover,
|
||||
[enabled]:hover {
|
||||
color: var(--card-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__tertiary {
|
||||
color: var(--card-color-tertiary);
|
||||
|
||||
&[href]:hover {
|
||||
color: var(--card-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Cover section for promoted (first) card.
|
||||
&__cover {
|
||||
display: block;
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding-bottom: 42%;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: var(--story-padding);
|
||||
}
|
||||
|
||||
&__indention {
|
||||
padding-left: calc(
|
||||
var(--su-7) + var(--su-2)
|
||||
); // avatar_width + avatar_margin
|
||||
}
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--su-2);
|
||||
}
|
||||
|
||||
&__overflow {
|
||||
color: var(--card-color-tertiary);
|
||||
@media (min-width: $breakpoint-s) {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--card-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.crayons-story__overflow {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: var(--lh-tight);
|
||||
font-size: var(--fs-s);
|
||||
}
|
||||
|
||||
&__author-pic {
|
||||
position: relative;
|
||||
margin-right: var(--su-2);
|
||||
}
|
||||
|
||||
&__tags {
|
||||
margin-bottom: var(--su-2);
|
||||
margin-left: calc(var(--su-1) * -1);
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--card-color);
|
||||
line-height: var(--lh-tight);
|
||||
margin-bottom: var(--su-2);
|
||||
font-size: var(--title-font-size);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__flare-tag {
|
||||
font-weight: normal;
|
||||
font-family: var(--ff-accent);
|
||||
font-size: 0.6em;
|
||||
padding: 0.3em;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
line-height: 1em;
|
||||
display: inline-block;
|
||||
border-radius: var(--radius);
|
||||
letter-spacing: -0.05em;
|
||||
background: var(--base-60);
|
||||
margin-right: var(--su-1);
|
||||
color: var(--base-inverted);
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--fs-s);
|
||||
line-height: var(--lh-tight);
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: flex;
|
||||
margin-left: calc(var(--su-2) * -1);
|
||||
}
|
||||
|
||||
&__comments {
|
||||
cursor: default;
|
||||
background: var(--story-comments-bg);
|
||||
padding: var(--story-padding);
|
||||
padding-top: var(--su-4);
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
margin-top: calc(var(--su-1) * -1);
|
||||
|
||||
&__headline {
|
||||
font-weight: var(--fw-bold);
|
||||
font-size: var(--fs-s);
|
||||
color: var(--card-color-secondary);
|
||||
margin-bottom: var(--su-3);
|
||||
padding-left: calc(var(--su-6) + var(--su-4));
|
||||
}
|
||||
|
||||
&__actions {
|
||||
padding-left: calc(var(--su-6) + var(--su-4));
|
||||
padding-top: var(--su-4);
|
||||
}
|
||||
}
|
||||
|
||||
&__snippet {
|
||||
color: var(--card-secondary-color);
|
||||
font-size: inherit;
|
||||
em {
|
||||
font-weight: var(--fw-medium);
|
||||
color: var(--card-color);
|
||||
background: var(--accent-warning-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
&__headline {
|
||||
font-family: var(--ff-accent);
|
||||
font-size: var(--fs-s);
|
||||
color: var(--card-color-tertiary);
|
||||
margin-bottom: var(--su-4);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__save {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__video {
|
||||
position: relative;
|
||||
padding-top: 56%;
|
||||
display: block;
|
||||
background-color: var(--base-100);
|
||||
background-size: cover;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__time {
|
||||
position: absolute;
|
||||
bottom: var(--su-4);
|
||||
right: var(--su-4);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: var(--base-inverted);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--su-1);
|
||||
font-size: var(--fs-s);
|
||||
font-family: var(--ff-accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-podcast-episode {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--su-4);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__cover {
|
||||
width: var(--su-7);
|
||||
height: var(--su-7);
|
||||
margin-right: var(--su-2);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
&__author {
|
||||
font-size: var(--fs-s);
|
||||
color: var(--card-color-secondary);
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--card-color);
|
||||
line-height: var(--lh-tight);
|
||||
margin-bottom: var(--su-1);
|
||||
font-size: var(--fs-xl);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
font-size: var(--fs-2xl);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy
|
||||
.crayons-story {
|
||||
.crayons-btn {
|
||||
.bm-initial {
|
||||
display: inline-flex;
|
||||
}
|
||||
.bm-success {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.bm-initial {
|
||||
display: none;
|
||||
}
|
||||
.bm-success {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
app/assets/stylesheets/components/tags.scss
Normal file
24
app/assets/stylesheets/components/tags.scss
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
@import '../config/import';
|
||||
|
||||
.crayons-tag {
|
||||
font-family: var(--ff-accent);
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--tag-color);
|
||||
line-height: var(--lh-tight);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--su-1);
|
||||
display: inline-block;
|
||||
letter-spacing: -0.05em;
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
font-size: var(--fs-s);
|
||||
}
|
||||
|
||||
&__prefix {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--tag-color-hover);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,9 @@
|
|||
|
||||
// Main colors
|
||||
--body-bg: #f9fafa; // todo: replace with `var(--base-10);` when ready.
|
||||
--body-color: var(--base-100);
|
||||
--body-color: var(
|
||||
--base-100
|
||||
); // todo: replace with `var(--base-100);` when ready.
|
||||
--body-color-inverted: var(--base-inverted);
|
||||
|
||||
// Main content containers
|
||||
|
|
@ -187,4 +189,12 @@
|
|||
--indicator-critical-color: var(--body-color-inverted);
|
||||
--indicator-default-bg: var(--button-secondary-bg);
|
||||
--indicator-default-color: var(--button-secondary-color);
|
||||
|
||||
// Tags
|
||||
--tag-color: var(--base-70);
|
||||
--tag-color-hover: var(--base-100);
|
||||
|
||||
// Story
|
||||
--story-comments-bg: var(--base-0);
|
||||
--story-border: transparent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,14 @@
|
|||
@import 'components/banners';
|
||||
@import 'components/buttons';
|
||||
@import 'components/cards';
|
||||
@import 'components/comments';
|
||||
@import 'components/dropdowns';
|
||||
@import 'components/forms';
|
||||
@import 'components/indicators';
|
||||
@import 'components/loaders';
|
||||
@import 'components/modals';
|
||||
@import 'components/navigation';
|
||||
@import 'components/notices';
|
||||
@import 'components/snackbars';
|
||||
@import 'components/stories';
|
||||
@import 'components/tags';
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
--card-color: var(--accent-brand);
|
||||
--card-color-secondary: var(--accent-brand-darker);
|
||||
--card-color-tertiary: var(--base-50);
|
||||
--card-secondary-bg: var(--base-10);
|
||||
--card-secondary-bg: var(--base-inverted);
|
||||
--card-secondary-color: var(--base-90);
|
||||
--card-headline-color: var(--accent-brand-lighter);
|
||||
|
||||
|
|
@ -179,4 +179,12 @@
|
|||
--indicator-critical-color: var(--body-color-inverted);
|
||||
--indicator-default-bg: var(--button-secondary-bg);
|
||||
--indicator-default-color: var(--button-secondary-color);
|
||||
|
||||
// Tags
|
||||
--tag-color: var(--base-70);
|
||||
--tag-color-hover: var(--base-100);
|
||||
|
||||
// Story
|
||||
--story-comments-bg: rgba(255, 255, 255, 0.05);
|
||||
--story-border: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,4 +177,12 @@
|
|||
--indicator-critical-color: var(--body-color-inverted);
|
||||
--indicator-default-bg: var(--button-secondary-bg);
|
||||
--indicator-default-color: var(--button-secondary-color);
|
||||
|
||||
// Tags
|
||||
--tag-color: var(--base-70);
|
||||
--tag-color-hover: var(--base-100);
|
||||
|
||||
// Story
|
||||
--story-comments-bg: rgba(0, 0, 0, 0.02);
|
||||
--story-border: transparent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
--card-color: var(--base-100);
|
||||
--card-color-secondary: var(--base-70);
|
||||
--card-color-tertiary: var(--base-50);
|
||||
--card-secondary-bg: var(--base-10);
|
||||
--card-secondary-bg: var(--base-a5);
|
||||
--card-secondary-color: var(--base-90);
|
||||
--card-headline-color: var(--base-100);
|
||||
|
||||
|
|
@ -179,4 +179,12 @@
|
|||
--indicator-critical-color: var(--body-color);
|
||||
--indicator-default-bg: var(--button-secondary-bg);
|
||||
--indicator-default-color: var(--button-secondary-color);
|
||||
|
||||
// Tags
|
||||
--tag-color: var(--base-70);
|
||||
--tag-color-hover: var(--base-100);
|
||||
|
||||
// Story
|
||||
--story-comments-bg: rgba(0, 0, 0, 0.2);
|
||||
--story-border: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,4 +177,12 @@
|
|||
--indicator-critical-color: var(--body-color-inverted);
|
||||
--indicator-dimmed-bg: var(--base-30);
|
||||
--indicator-dimmed-color: var(--base-70);
|
||||
|
||||
// Tags
|
||||
--tag-color: var(--base-70);
|
||||
--tag-color-hover: var(--base-100);
|
||||
|
||||
// Story
|
||||
--story-comments-bg: rgba(255, 255, 255, 0.05);
|
||||
--story-border: var(--accent-brand);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,8 @@ class CommentDecorator < ApplicationDecorator
|
|||
|
||||
created_at.utc.iso8601
|
||||
end
|
||||
|
||||
def published_at_int
|
||||
created_at.to_i
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,141 +2,121 @@ import { h } from 'preact';
|
|||
import PropTypes from 'prop-types';
|
||||
import { articlePropTypes } from '../src/components/common-prop-types/article-prop-types';
|
||||
import {
|
||||
ArticleCoverImage,
|
||||
CommentsCount,
|
||||
CommentsList,
|
||||
ContentTitle,
|
||||
OrganizationHeadline,
|
||||
PublishDate,
|
||||
ReadingTime,
|
||||
Meta,
|
||||
SaveButton,
|
||||
SearchSnippet,
|
||||
TagList,
|
||||
ReactionsCount,
|
||||
ReadingTime,
|
||||
Video,
|
||||
} from './components';
|
||||
import { PodcastArticle } from './PodcastArticle';
|
||||
|
||||
/* global timeAgo */
|
||||
|
||||
export const Article = ({
|
||||
article,
|
||||
currentTag,
|
||||
isFeatured,
|
||||
isBookmarked,
|
||||
reactionsIcon,
|
||||
commentsIcon,
|
||||
videoIcon,
|
||||
bookmarkClick,
|
||||
}) => {
|
||||
if (article && article.type_of === 'podcast_episodes') {
|
||||
return <PodcastArticle article={article} />;
|
||||
}
|
||||
|
||||
const timeAgoIndicator = timeAgo({
|
||||
oldTimeInSeconds: article.published_at_int,
|
||||
formatter: (x) => x,
|
||||
});
|
||||
const clickableClassList = [
|
||||
'crayons-story',
|
||||
'crayons-story__top',
|
||||
'crayons-story__body',
|
||||
'crayons-story__indention',
|
||||
'crayons-story__title',
|
||||
'crayons-story__tags',
|
||||
'crayons-story__bottom',
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="single-article single-article-small-pic"
|
||||
<article
|
||||
className={`crayons-story cursor-pointer ${
|
||||
isFeatured && 'crayons-story--featured'
|
||||
}`}
|
||||
id={isFeatured && 'featured-story-marker'}
|
||||
data-featured-article={isFeatured && `articles-${article.id}`}
|
||||
data-content-user-id={article.user_id}
|
||||
>
|
||||
{article.cloudinary_video_url && (
|
||||
<a
|
||||
href={article.path}
|
||||
className="single-article-video-preview"
|
||||
style={`background-image:url(${article.cloudinary_video_url})`}
|
||||
>
|
||||
<div className="single-article-video-duration">
|
||||
<img src={videoIcon} alt="video camera" loading="lazy" />
|
||||
{article.video_duration_in_minutes}
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
<div
|
||||
role="presentation"
|
||||
onClick={(event) => {
|
||||
const { classList } = event.target;
|
||||
|
||||
<OrganizationHeadline organization={article.organization} />
|
||||
<div className="small-pic">
|
||||
<a
|
||||
href={`/${article.user.username}`}
|
||||
className="small-pic-link-wrapper"
|
||||
>
|
||||
<img
|
||||
src={article.user.profile_image_90}
|
||||
alt={`${article.user.username} profile`}
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href={article.path}
|
||||
className="small-pic-link-wrapper index-article-link"
|
||||
id={`article-link-${article.id}`}
|
||||
if (clickableClassList.includes(...classList)) {
|
||||
InstantClick.preload(article.path);
|
||||
InstantClick.display(article.path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="content">
|
||||
<ContentTitle article={article} currentTag={currentTag} />
|
||||
{article.class_name === 'Article' && (
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
<SearchSnippet highlightText={article.highlight} />
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
<h4>
|
||||
<a href={`/${article.user.username}`}>
|
||||
{filterXSS(
|
||||
article.class_name === 'User'
|
||||
? article.user.username
|
||||
: article.user.name,
|
||||
)}
|
||||
{article.readable_publish_date ? '・' : ''}
|
||||
{article.readable_publish_date && (
|
||||
<PublishDate
|
||||
readablePublishDate={article.readable_publish_date}
|
||||
publishedTimestamp={article.published_timestamp}
|
||||
/>
|
||||
)}
|
||||
{article.published_at_int ? (
|
||||
<span className="time-ago-indicator">
|
||||
{timeAgoIndicator.length > 0 ? `(${timeAgoIndicator})` : ''}
|
||||
</span>
|
||||
) : null}
|
||||
</a>
|
||||
</h4>
|
||||
{article.cloudinary_video_url && <Video article={article} />}
|
||||
|
||||
<TagList tags={article.tag_list} />
|
||||
{article.class_name !== 'User' && (
|
||||
<CommentsCount
|
||||
count={article.comments_count}
|
||||
articlePath={article.path}
|
||||
icon={commentsIcon}
|
||||
/>
|
||||
)}
|
||||
{article.class_name !== 'User' && (
|
||||
<ReactionsCount article={article} icon={reactionsIcon} />
|
||||
)}
|
||||
{article.class_name === 'Article' && (
|
||||
<ReadingTime
|
||||
articlePath={article.path}
|
||||
readingTime={article.reading_time}
|
||||
/>
|
||||
)}
|
||||
<SaveButton
|
||||
article={article}
|
||||
isBookmarked={isBookmarked}
|
||||
onClick={bookmarkClick}
|
||||
/>
|
||||
</div>
|
||||
{isFeatured && <ArticleCoverImage article={article} />}
|
||||
<div className="crayons-story__body">
|
||||
<div className="crayons-story__top">
|
||||
<Meta article={article} organization={article.organization} />
|
||||
</div>
|
||||
|
||||
<div className="crayons-story__indention">
|
||||
<ContentTitle article={article} />
|
||||
<TagList tags={article.tag_list} />
|
||||
|
||||
{article.class_name === 'Article' && (
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
<SearchSnippet highlightText={article.highlight} />
|
||||
)}
|
||||
|
||||
<div className="crayons-story__bottom">
|
||||
{article.class_name !== 'User' && (
|
||||
<div className="crayons-story__details">
|
||||
<ReactionsCount article={article} />
|
||||
<CommentsCount
|
||||
count={article.comments_count}
|
||||
articlePath={article.path}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="crayons-story__save">
|
||||
<ReadingTime readingTime={article.reading_time} />
|
||||
|
||||
<SaveButton
|
||||
article={article}
|
||||
isBookmarked={isBookmarked}
|
||||
onClick={bookmarkClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{article.top_comments && article.top_comments.length > 0 && (
|
||||
<CommentsList
|
||||
comments={article.top_comments}
|
||||
articlePath={article.path}
|
||||
totalCount={article.comments_count}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
Article.defaultProps = {
|
||||
currentTag: null,
|
||||
isBookmarked: false,
|
||||
isFeatured: false,
|
||||
};
|
||||
|
||||
Article.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
currentTag: PropTypes.string,
|
||||
isBookmarked: PropTypes.bool,
|
||||
reactionsIcon: PropTypes.string.isRequired,
|
||||
commentsIcon: PropTypes.string.isRequired,
|
||||
videoIcon: PropTypes.string.isRequired,
|
||||
isFeatured: PropTypes.bool,
|
||||
bookmarkClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
TagList,
|
||||
SaveButton,
|
||||
ReadingTime,
|
||||
ReactionsCount,
|
||||
CommentsCount,
|
||||
} from './components';
|
||||
import { articlePropTypes } from '../src/components/common-prop-types';
|
||||
|
||||
export const FeaturedArticle = ({
|
||||
article,
|
||||
isBookmarked,
|
||||
reactionsIcon,
|
||||
commentsIcon,
|
||||
bookmarkClick,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id="featured-story-marker"
|
||||
data-featured-article={`articles-${article.id}`}
|
||||
/>
|
||||
<img
|
||||
src={article.main_image}
|
||||
style={{ display: 'none' }}
|
||||
alt={article.title}
|
||||
/>
|
||||
<a
|
||||
href={article.path}
|
||||
id={`article-link-${article.id}`}
|
||||
className="index-article-link"
|
||||
aria-label="Main Story"
|
||||
data-featured-article={`articles-${article.id}`}
|
||||
/>
|
||||
<div
|
||||
className="single-article big-article"
|
||||
data-content-user-id={article.user_id}
|
||||
>
|
||||
<a
|
||||
href={article.path}
|
||||
id={`article-link-${article.id}`}
|
||||
className="index-article-link"
|
||||
aria-label="Main Story"
|
||||
data-featured-article={`articles-${article.id}`}
|
||||
>
|
||||
<div
|
||||
className="picture image-final"
|
||||
style={{
|
||||
backgroundColor: article.main_image_background_hex_color,
|
||||
backgroundImage: `url(${article.main_image})`,
|
||||
}}
|
||||
/>
|
||||
<div className="content-wrapper">
|
||||
<h3>{article.title}</h3>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`/${article.user.username}`}
|
||||
className="featured-profile-button"
|
||||
>
|
||||
<img
|
||||
className="featured-profile-pic"
|
||||
src={article.user.profile_image_90}
|
||||
alt={article.title}
|
||||
/>
|
||||
</a>
|
||||
<div className="featured-user-name">
|
||||
<a href={`/${article.user.username}`}>
|
||||
{article.user.name} ・{' '}
|
||||
<time dateTime={article.published_timestamp}>
|
||||
{article.readable_publish_date}
|
||||
</time>
|
||||
<span
|
||||
className="time-ago-indicator-initial-placeholder"
|
||||
data-seconds={`${article.published_at_int}`}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<TagList tags={article.tag_list} className="featured-tags" />
|
||||
<CommentsCount
|
||||
count={article.comments_count}
|
||||
articlePath={article.path}
|
||||
icon={commentsIcon}
|
||||
className="featured-engagement-count"
|
||||
/>
|
||||
<ReactionsCount article={article} icon={reactionsIcon} />
|
||||
<ReadingTime
|
||||
articlePath={article.path}
|
||||
readingTime={article.reading_time}
|
||||
/>
|
||||
<SaveButton
|
||||
article={article}
|
||||
isBookmarked={isBookmarked}
|
||||
onClick={bookmarkClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FeaturedArticle.defaultProps = {
|
||||
isBookmarked: false,
|
||||
};
|
||||
|
||||
FeaturedArticle.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
isBookmarked: PropTypes.bool,
|
||||
reactionsIcon: PropTypes.string.isRequired,
|
||||
commentsIcon: PropTypes.string.isRequired,
|
||||
bookmarkClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FEED_ICONS } from '../packs/feedIcons.js.erb';
|
||||
|
||||
/* global userData sendHapticMessage showModal buttonFormData renderNewSidebarCount */
|
||||
|
||||
|
|
@ -145,7 +144,6 @@ export class Feed extends Component {
|
|||
>
|
||||
{renderFeed({
|
||||
feedItems,
|
||||
feedIcons: FEED_ICONS,
|
||||
podcastEpisodes,
|
||||
bookmarkedFeedItems,
|
||||
bookmarkClick: this.bookmarkClick,
|
||||
|
|
|
|||
|
|
@ -2,29 +2,30 @@ import { h } from 'preact';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export const LoadingArticle = ({ version }) => {
|
||||
const bigPicArea = version === 'featured' ? <div className="picture" /> : '';
|
||||
const smallPic =
|
||||
version === 'featured' ? (
|
||||
''
|
||||
) : (
|
||||
<div className="small-pic">
|
||||
<div className="color single-article-loading" />
|
||||
const cover =
|
||||
version === 'featured' && (
|
||||
<div className="crayons-story__cover">
|
||||
<div
|
||||
className="crayons-scaffold crayons-story__cover__image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="single-article single-article-small-pic big-article"
|
||||
style={{ paddingBottom: version === 'featured' ? '46px' : '0' }}
|
||||
>
|
||||
{bigPicArea}
|
||||
{smallPic}
|
||||
<div className="content">
|
||||
<h3 className="single-article-loading"> </h3>
|
||||
<div className="crayons-story">
|
||||
{cover}
|
||||
<div className="crayons-story__body">
|
||||
<div className="crayons-story__top mb-3">
|
||||
<div className="crayons-story__meta w-100">
|
||||
<div className="crayons-scaffold-loading mr-2 w-0 h-0 p-4 radius-full" />
|
||||
<div className="crayons-scaffold-loading w-25 h-0 py-2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="crayons-story__indention">
|
||||
<div className="crayons-scaffold-loading w-75 h-0 py-3 mb-2" />
|
||||
<div className="crayons-scaffold-loading w-50 h-0 py-2 mb-8" />
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="single-article-loading" style={{ width: '46%' }}>
|
||||
|
||||
</h4>
|
||||
<div className="tags single-article-loading" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,21 +6,14 @@ import {
|
|||
article,
|
||||
articleWithOrganization,
|
||||
articleWithSnippetResult,
|
||||
articleWithReadingTimeGreaterThan1,
|
||||
articleWithReactions,
|
||||
articleWithComments,
|
||||
assetPath,
|
||||
featuredArticle,
|
||||
} from '../__tests__/utilities/articleUtilities';
|
||||
import { articleDecorator } from './articleDecorator';
|
||||
|
||||
import '../../../assets/stylesheets/articles.scss';
|
||||
|
||||
const ICONS = {
|
||||
REACTIONS_ICON: assetPath('reactions-stack.png'),
|
||||
COMMENTS_ICON: assetPath('comments-bubble.png'),
|
||||
VIDEO_ICON: assetPath('video-camera.svg'),
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
bookmarkClick: action('Saved/unsaved article'),
|
||||
};
|
||||
|
|
@ -34,9 +27,6 @@ export default {
|
|||
export const DefaultArticle = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', article)}
|
||||
currentTag={text('currentTag', 'javascript')}
|
||||
|
|
@ -47,12 +37,23 @@ DefaultArticle.story = {
|
|||
name: 'default',
|
||||
};
|
||||
|
||||
export const IsFeatured = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
isFeatured={boolean('isFeatured', true)}
|
||||
article={object('article', featuredArticle)}
|
||||
currentTag={text('currentTag', 'javascript')}
|
||||
/>
|
||||
);
|
||||
|
||||
IsFeatured.story = {
|
||||
name: 'is featured',
|
||||
};
|
||||
|
||||
export const WithOrganization = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', articleWithOrganization)}
|
||||
currentTag={text('currentTag', 'javascript')}
|
||||
|
|
@ -66,9 +67,6 @@ WithOrganization.story = {
|
|||
export const WithFlareTag = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', article)}
|
||||
currentTag={text('currentTag')}
|
||||
|
|
@ -82,9 +80,6 @@ WithFlareTag.story = {
|
|||
export const WithSnippetResult = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', articleWithSnippetResult)}
|
||||
currentTag={text('currentTag')}
|
||||
|
|
@ -95,28 +90,9 @@ WithSnippetResult.story = {
|
|||
name: 'with snippet result',
|
||||
};
|
||||
|
||||
export const WithReadingTime = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', articleWithReadingTimeGreaterThan1)}
|
||||
currentTag={text('currentTag')}
|
||||
/>
|
||||
);
|
||||
|
||||
WithReadingTime.story = {
|
||||
name: 'with reading time',
|
||||
};
|
||||
|
||||
export const WithReactions = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', articleWithReactions)}
|
||||
currentTag={text('currentTag')}
|
||||
|
|
@ -130,9 +106,6 @@ WithReactions.story = {
|
|||
export const WithComments = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', false)}
|
||||
article={object('article', articleWithComments)}
|
||||
currentTag={text('currentTag')}
|
||||
|
|
@ -146,9 +119,6 @@ WithComments.story = {
|
|||
export const OnReadingList = () => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', true)}
|
||||
article={object('article', articleWithComments)}
|
||||
currentTag={text('currentTag')}
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { withKnobs, object, text, boolean } from '@storybook/addon-knobs/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import {
|
||||
featuredArticle,
|
||||
assetPath,
|
||||
} from '../__tests__/utilities/articleUtilities';
|
||||
import { FeaturedArticle } from '..';
|
||||
import { articleDecorator } from './articleDecorator';
|
||||
|
||||
import '../../../assets/stylesheets/articles.scss';
|
||||
|
||||
const ICONS = {
|
||||
REACTIONS_ICON: assetPath('reactions-stack.png'),
|
||||
COMMENTS_ICON: assetPath('comments-bubble.png'),
|
||||
VIDEO_ICON: assetPath('video-camera.svg'),
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
bookmarkClick: action('Saved/unsaved article'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'App Components/Article/Featured',
|
||||
decorators: [withKnobs, articleDecorator],
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<FeaturedArticle
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
videoIcon={text('videoIcon', ICONS.VIDEO_ICON)}
|
||||
article={object('article', featuredArticle)}
|
||||
/>
|
||||
);
|
||||
|
||||
Default.story = { name: 'default' };
|
||||
|
||||
export const OnReadingList = () => (
|
||||
<FeaturedArticle
|
||||
{...commonProps}
|
||||
reactionsIcon={text('reactionsIcon', ICONS.REACTIONS_ICON)}
|
||||
commentsIcon={text('commentsIcon', ICONS.COMMENTS_ICON)}
|
||||
isBookmarked={boolean('isBookmarked', true)}
|
||||
article={object('article', featuredArticle)}
|
||||
/>
|
||||
);
|
||||
|
||||
OnReadingList.story = { name: 'is on reading List' };
|
||||
|
|
@ -4,20 +4,21 @@ import '../../../assets/stylesheets/articles.scss';
|
|||
import { articleDecorator } from './articleDecorator';
|
||||
|
||||
export default {
|
||||
title: 'App Components/Article',
|
||||
title: 'App Components/Article Loading',
|
||||
component: LoadingArticle,
|
||||
decorators: [articleDecorator],
|
||||
};
|
||||
|
||||
export const ArticleLoading = () => <LoadingArticle />;
|
||||
export const DefaultArticle = () => <LoadingArticle />;
|
||||
|
||||
ArticleLoading.story = {
|
||||
name: 'article loading',
|
||||
DefaultArticle.story = {
|
||||
name: 'default',
|
||||
};
|
||||
|
||||
export const FeaturedArticleLoading = () => (
|
||||
export const FeaturedLoading = () => (
|
||||
<LoadingArticle version="featured" />
|
||||
);
|
||||
|
||||
FeaturedArticleLoading.story = {
|
||||
name: 'featured article loading',
|
||||
FeaturedLoading.story = {
|
||||
name: 'featured',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
article,
|
||||
articleWithOrganization,
|
||||
articleWithSnippetResult,
|
||||
articleWithReadingTimeGreaterThan1,
|
||||
articleWithReactions,
|
||||
videoArticle,
|
||||
articleWithComments,
|
||||
|
|
@ -36,6 +35,19 @@ describe('<Article /> component', () => {
|
|||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a featured article', () => {
|
||||
const tree = render(
|
||||
<Article
|
||||
{...commonProps}
|
||||
isBookmarked={false}
|
||||
isFeatured
|
||||
article={article}
|
||||
currentTag="javascript"
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with an organization', () => {
|
||||
const tree = render(
|
||||
<Article
|
||||
|
|
@ -66,17 +78,6 @@ describe('<Article /> component', () => {
|
|||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with a reading time', () => {
|
||||
const tree = render(
|
||||
<Article
|
||||
{...commonProps}
|
||||
isBookmarked={false}
|
||||
article={articleWithReadingTimeGreaterThan1}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with reactions', () => {
|
||||
const tree = render(
|
||||
<Article
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { featuredArticle, assetPath } from './utilities/articleUtilities';
|
||||
import '../../../assets/javascripts/lib/xss';
|
||||
import '../../../assets/javascripts/utilities/timeAgo';
|
||||
import { FeaturedArticle } from '../FeaturedArticle';
|
||||
|
||||
const featuredArticleNoReactions = {
|
||||
...featuredArticle,
|
||||
positive_reactions_count: 0,
|
||||
};
|
||||
const commonProps = {
|
||||
reactionsIcon: assetPath('reactions-stack.png'),
|
||||
commentsIcon: assetPath('comments-bubble.png'),
|
||||
};
|
||||
|
||||
describe('<FeaturedArticle /> component', () => {
|
||||
it('should render a standard article', () => {
|
||||
const tree = render(
|
||||
<FeaturedArticle
|
||||
{...commonProps}
|
||||
isBookmarked={false}
|
||||
article={featuredArticle}
|
||||
currentTag="javascript"
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with the reactions icon when the reactions count is 0', () => {
|
||||
const tree = render(
|
||||
<FeaturedArticle
|
||||
{...commonProps}
|
||||
isBookmarked={false}
|
||||
article={featuredArticleNoReactions}
|
||||
currentTag="javascript"
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a bookmarked featured article', () => {
|
||||
const tree = render(
|
||||
<FeaturedArticle
|
||||
{...commonProps}
|
||||
isBookmarked
|
||||
article={featuredArticle}
|
||||
currentTag="javascript"
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,41 +2,35 @@
|
|||
|
||||
exports[`<LoadingArticle /> should render 1`] = `
|
||||
<div
|
||||
class="single-article single-article-small-pic big-article"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": "0",
|
||||
}
|
||||
}
|
||||
class="crayons-story"
|
||||
>
|
||||
<div
|
||||
class="small-pic"
|
||||
class="crayons-story__body"
|
||||
>
|
||||
<div
|
||||
class="color single-article-loading"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<h3
|
||||
class="single-article-loading"
|
||||
class="crayons-story__top mb-3"
|
||||
>
|
||||
|
||||
</h3>
|
||||
<div
|
||||
class="crayons-story__meta w-100"
|
||||
>
|
||||
<div
|
||||
class="crayons-scaffold-loading mr-2 w-0 h-0 p-4 radius-full"
|
||||
/>
|
||||
<div
|
||||
class="crayons-scaffold-loading w-25 h-0 py-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-story__indention"
|
||||
>
|
||||
<div
|
||||
class="crayons-scaffold-loading w-75 h-0 py-3 mb-2"
|
||||
/>
|
||||
<div
|
||||
class="crayons-scaffold-loading w-50 h-0 py-2 mb-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h4
|
||||
class="single-article-loading"
|
||||
style={
|
||||
Object {
|
||||
"width": "46%",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</h4>
|
||||
<div
|
||||
class="tags single-article-loading"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,511 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FeaturedArticle /> component should render a bookmarked featured article 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-featured-article="articles-62407"
|
||||
id="featured-story-marker"
|
||||
/>
|
||||
<img
|
||||
alt="Unbranded Home Loan Account"
|
||||
src="/images/onboarding-background.png"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<a
|
||||
aria-label="Main Story"
|
||||
class="index-article-link"
|
||||
data-featured-article="articles-62407"
|
||||
href="/some-post/path"
|
||||
id="article-link-62407"
|
||||
/>
|
||||
<div
|
||||
class="single-article big-article"
|
||||
data-content-user-id={23289}
|
||||
>
|
||||
<a
|
||||
aria-label="Main Story"
|
||||
class="index-article-link"
|
||||
data-featured-article="articles-62407"
|
||||
href="/some-post/path"
|
||||
id="article-link-62407"
|
||||
>
|
||||
<div
|
||||
class="picture image-final"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": undefined,
|
||||
"backgroundImage": "url(/images/onboarding-background.png)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
>
|
||||
<h3>
|
||||
Unbranded Home Loan Account
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="featured-profile-button"
|
||||
href="/Emil99"
|
||||
>
|
||||
<img
|
||||
alt="Unbranded Home Loan Account"
|
||||
class="featured-profile-pic"
|
||||
src="/images/10.png"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="featured-user-name"
|
||||
>
|
||||
<a
|
||||
href="/Emil99"
|
||||
>
|
||||
Stella Macejkovic ・
|
||||
<time
|
||||
dateTime="Tue, 18 Feb 2020 14:59:24 GMT"
|
||||
>
|
||||
February 18
|
||||
</time>
|
||||
<span
|
||||
class="time-ago-indicator-initial-placeholder"
|
||||
data-seconds="1582037964819"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="tags featured-tags"
|
||||
>
|
||||
<a
|
||||
href="/t/javascript"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#javascript
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/t/ruby"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#ruby
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/t/go"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#go
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="article-engagement-count comments-count featured-engagement-count"
|
||||
>
|
||||
<a
|
||||
href="/some-post/path#comments"
|
||||
>
|
||||
<img
|
||||
alt="chat"
|
||||
loading="lazy"
|
||||
src="/images/comments-bubble.png"
|
||||
/>
|
||||
<span
|
||||
class="engagement-count-number"
|
||||
>
|
||||
213
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="article-engagement-count reactions-count"
|
||||
>
|
||||
<a
|
||||
href="/some-post/path"
|
||||
>
|
||||
<img
|
||||
alt="heart"
|
||||
loading="lazy"
|
||||
src="/images/reactions-stack.png"
|
||||
/>
|
||||
<span
|
||||
class="engagement-count-number"
|
||||
id="engagement-count-number-62407"
|
||||
>
|
||||
428
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="article-reading-time"
|
||||
href="/some-post/path"
|
||||
>
|
||||
undefined min read
|
||||
</a>
|
||||
<button
|
||||
class="article-engagement-count engage-button bookmark-button selected"
|
||||
data-initial-feed={true}
|
||||
data-reactable-id={62407}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onMouseout={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bm-success"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FeaturedArticle /> component should render a standard article 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-featured-article="articles-62407"
|
||||
id="featured-story-marker"
|
||||
/>
|
||||
<img
|
||||
alt="Unbranded Home Loan Account"
|
||||
src="/images/onboarding-background.png"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<a
|
||||
aria-label="Main Story"
|
||||
class="index-article-link"
|
||||
data-featured-article="articles-62407"
|
||||
href="/some-post/path"
|
||||
id="article-link-62407"
|
||||
/>
|
||||
<div
|
||||
class="single-article big-article"
|
||||
data-content-user-id={23289}
|
||||
>
|
||||
<a
|
||||
aria-label="Main Story"
|
||||
class="index-article-link"
|
||||
data-featured-article="articles-62407"
|
||||
href="/some-post/path"
|
||||
id="article-link-62407"
|
||||
>
|
||||
<div
|
||||
class="picture image-final"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": undefined,
|
||||
"backgroundImage": "url(/images/onboarding-background.png)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
>
|
||||
<h3>
|
||||
Unbranded Home Loan Account
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="featured-profile-button"
|
||||
href="/Emil99"
|
||||
>
|
||||
<img
|
||||
alt="Unbranded Home Loan Account"
|
||||
class="featured-profile-pic"
|
||||
src="/images/10.png"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="featured-user-name"
|
||||
>
|
||||
<a
|
||||
href="/Emil99"
|
||||
>
|
||||
Stella Macejkovic ・
|
||||
<time
|
||||
dateTime="Tue, 18 Feb 2020 14:59:24 GMT"
|
||||
>
|
||||
February 18
|
||||
</time>
|
||||
<span
|
||||
class="time-ago-indicator-initial-placeholder"
|
||||
data-seconds="1582037964819"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="tags featured-tags"
|
||||
>
|
||||
<a
|
||||
href="/t/javascript"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#javascript
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/t/ruby"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#ruby
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/t/go"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#go
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="article-engagement-count comments-count featured-engagement-count"
|
||||
>
|
||||
<a
|
||||
href="/some-post/path#comments"
|
||||
>
|
||||
<img
|
||||
alt="chat"
|
||||
loading="lazy"
|
||||
src="/images/comments-bubble.png"
|
||||
/>
|
||||
<span
|
||||
class="engagement-count-number"
|
||||
>
|
||||
213
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="article-engagement-count reactions-count"
|
||||
>
|
||||
<a
|
||||
href="/some-post/path"
|
||||
>
|
||||
<img
|
||||
alt="heart"
|
||||
loading="lazy"
|
||||
src="/images/reactions-stack.png"
|
||||
/>
|
||||
<span
|
||||
class="engagement-count-number"
|
||||
id="engagement-count-number-62407"
|
||||
>
|
||||
428
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="article-reading-time"
|
||||
href="/some-post/path"
|
||||
>
|
||||
undefined min read
|
||||
</a>
|
||||
<button
|
||||
class="article-engagement-count engage-button bookmark-button "
|
||||
data-initial-feed={true}
|
||||
data-reactable-id={62407}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onMouseout={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bm-initial"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FeaturedArticle /> component should render with the reactions icon when the reactions count is 0 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-featured-article="articles-62407"
|
||||
id="featured-story-marker"
|
||||
/>
|
||||
<img
|
||||
alt="Unbranded Home Loan Account"
|
||||
src="/images/onboarding-background.png"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<a
|
||||
aria-label="Main Story"
|
||||
class="index-article-link"
|
||||
data-featured-article="articles-62407"
|
||||
href="/some-post/path"
|
||||
id="article-link-62407"
|
||||
/>
|
||||
<div
|
||||
class="single-article big-article"
|
||||
data-content-user-id={23289}
|
||||
>
|
||||
<a
|
||||
aria-label="Main Story"
|
||||
class="index-article-link"
|
||||
data-featured-article="articles-62407"
|
||||
href="/some-post/path"
|
||||
id="article-link-62407"
|
||||
>
|
||||
<div
|
||||
class="picture image-final"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": undefined,
|
||||
"backgroundImage": "url(/images/onboarding-background.png)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
>
|
||||
<h3>
|
||||
Unbranded Home Loan Account
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="featured-profile-button"
|
||||
href="/Emil99"
|
||||
>
|
||||
<img
|
||||
alt="Unbranded Home Loan Account"
|
||||
class="featured-profile-pic"
|
||||
src="/images/10.png"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="featured-user-name"
|
||||
>
|
||||
<a
|
||||
href="/Emil99"
|
||||
>
|
||||
Stella Macejkovic ・
|
||||
<time
|
||||
dateTime="Tue, 18 Feb 2020 14:59:24 GMT"
|
||||
>
|
||||
February 18
|
||||
</time>
|
||||
<span
|
||||
class="time-ago-indicator-initial-placeholder"
|
||||
data-seconds="1582037964819"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="tags featured-tags"
|
||||
>
|
||||
<a
|
||||
href="/t/javascript"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#javascript
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/t/ruby"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#ruby
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/t/go"
|
||||
>
|
||||
<span
|
||||
class="tag"
|
||||
>
|
||||
#go
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="article-engagement-count comments-count featured-engagement-count"
|
||||
>
|
||||
<a
|
||||
href="/some-post/path#comments"
|
||||
>
|
||||
<img
|
||||
alt="chat"
|
||||
loading="lazy"
|
||||
src="/images/comments-bubble.png"
|
||||
/>
|
||||
<span
|
||||
class="engagement-count-number"
|
||||
>
|
||||
213
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="article-engagement-count reactions-count"
|
||||
>
|
||||
<a
|
||||
href="/some-post/path"
|
||||
>
|
||||
<img
|
||||
alt="heart"
|
||||
loading="lazy"
|
||||
src="/images/reactions-stack.png"
|
||||
/>
|
||||
<span
|
||||
class="engagement-count-number"
|
||||
id="engagement-count-number-62407"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="article-reading-time"
|
||||
href="/some-post/path"
|
||||
>
|
||||
undefined min read
|
||||
</a>
|
||||
<button
|
||||
class="article-engagement-count engage-button bookmark-button "
|
||||
data-initial-feed={true}
|
||||
data-reactable-id={62407}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onMouseout={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bm-initial"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -26,6 +26,7 @@ export const article = {
|
|||
published_timestamp: 'Tue, 18 Feb 2020 14:59:24 GMT',
|
||||
published_at: '2020-02-18T14:59:24Z',
|
||||
readable_publish_date: 'February 18',
|
||||
top_comments: [],
|
||||
};
|
||||
|
||||
export const articleWithOrganization = {
|
||||
|
|
@ -53,6 +54,7 @@ export const articleWithOrganization = {
|
|||
published_at_int: 1582037964819,
|
||||
published_timestamp: 'Tue, 18 Feb 2020 14:59:24 GMT',
|
||||
readable_publish_date: 'February 18',
|
||||
top_comments: [],
|
||||
organization: {
|
||||
id: 87120,
|
||||
name: 'Web info-mediaries',
|
||||
|
|
@ -87,6 +89,7 @@ export const articleWithSnippetResult = {
|
|||
published_timestamp: 'Tue, 18 Feb 2020 14:59:24 GMT',
|
||||
published_at: '2020-02-18T14:59:24Z',
|
||||
readable_publish_date: 'February 18',
|
||||
top_comments: [],
|
||||
highlight: {
|
||||
body_text: [
|
||||
'copying Rest withdrawal Handcrafted multi-state Pre-emptive e-markets feed',
|
||||
|
|
@ -121,6 +124,7 @@ export const articleWithReactions = {
|
|||
published_timestamp: 'Tue, 18 Feb 2020 14:59:24 GMT',
|
||||
published_at: '2020-03-19T10:04:15-05:00',
|
||||
readable_publish_date: 'February 18',
|
||||
top_comments: [],
|
||||
positive_reactions_count: 232,
|
||||
};
|
||||
|
||||
|
|
@ -150,6 +154,20 @@ export const articleWithComments = {
|
|||
published_timestamp: 'Tue, 18 Feb 2020 14:59:24 GMT',
|
||||
published_at: '2020-03-19T10:04:15-05:00',
|
||||
readable_publish_date: 'February 18',
|
||||
top_comments: [
|
||||
{
|
||||
comment_id: 23,
|
||||
user_id: 6,
|
||||
published_timestamp: '2020-04-21T19:41:40Z',
|
||||
published_at_int: 1587498100,
|
||||
safe_processed_html: '<p>Kitsch hoodie artisan.</p>\n\n',
|
||||
path: '/naoma_dr_rice/comment/n',
|
||||
username: 'naoma_dr_rice',
|
||||
name: 'Dr. Naoma Rice',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/6/79732fcf-5655-426a-a7f0-18c737c3f8e8.png',
|
||||
},
|
||||
],
|
||||
positive_reactions_count: 428,
|
||||
comments_count: 213,
|
||||
};
|
||||
|
|
@ -180,6 +198,7 @@ export const articleWithReadingTimeGreaterThan1 = {
|
|||
published_timestamp: 'Tue, 18 Feb 2020 14:59:24 GMT',
|
||||
published_at: '2020-02-18T14:59:24Z',
|
||||
readable_publish_date: 'February 18',
|
||||
top_comments: [],
|
||||
reading_time: 8,
|
||||
};
|
||||
|
||||
|
|
|
|||
24
app/javascript/articles/components/ArticleCoverImage.jsx
Normal file
24
app/javascript/articles/components/ArticleCoverImage.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { h } from 'preact';
|
||||
import { articlePropTypes } from '../../src/components/common-prop-types';
|
||||
|
||||
export const ArticleCoverImage = ({ article }) => {
|
||||
return (
|
||||
<a
|
||||
href={article.path}
|
||||
className="crayons-story__cover"
|
||||
title={article.title}
|
||||
>
|
||||
<div
|
||||
style={{backgroundImage: `url(${article.main_image})`}}
|
||||
className="crayons-story__cover__image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
ArticleCoverImage.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
};
|
||||
|
||||
ArticleCoverImage.displayName = 'ArticleCoverImage';
|
||||
63
app/javascript/articles/components/CommentListItem.jsx
Normal file
63
app/javascript/articles/components/CommentListItem.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/* global timeAgo */
|
||||
|
||||
function userProfilePage(username) {
|
||||
const str = `/${username}`;
|
||||
return str;
|
||||
}
|
||||
|
||||
export const CommentListItem = ({ comment }) => (
|
||||
<div
|
||||
className="crayons-comment pl-2 cursor-pointer"
|
||||
role="presentation"
|
||||
onClick={(_event) => {
|
||||
InstantClick.preload(comment.path);
|
||||
InstantClick.display(comment.path);
|
||||
}}
|
||||
>
|
||||
<div className="crayons-comment__meta">
|
||||
<a
|
||||
href={userProfilePage(comment.username)}
|
||||
className="crayons-story__secondary fw-medium"
|
||||
>
|
||||
<span className="crayons-avatar mr-2">
|
||||
<img
|
||||
src={comment.profile_image_90}
|
||||
className="crayons-avatar__image"
|
||||
alt={`{comment.username} avatar`}
|
||||
/>
|
||||
</span>
|
||||
{comment.name}
|
||||
</a>
|
||||
<a href={comment.path} className="crayons-story__tertiary ml-1">
|
||||
<time>
|
||||
{timeAgo({
|
||||
oldTimeInSeconds: comment.published_at_int,
|
||||
formatter: (x) => x,
|
||||
maxDisplayedAge: NaN,
|
||||
})}
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="crayons-comment__body"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: comment.safe_processed_html }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
CommentListItem.displayName = 'CommentsListItem';
|
||||
|
||||
CommentListItem.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
published_at_int: PropTypes.number.isRequired,
|
||||
safe_processed_html: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
|
@ -1,19 +1,49 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '../../crayons/Button';
|
||||
|
||||
export const CommentsCount = ({ count, articlePath, icon, className }) => {
|
||||
export const CommentsCount = ({ count, articlePath }) => {
|
||||
const commentsSVG = () => (
|
||||
<svg
|
||||
className="crayons-icon"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M10.5 5h3a6 6 0 110 12v2.625c-3.75-1.5-9-3.75-9-8.625a6 6 0 016-6zM12 15.5h1.5a4.501 4.501 0 001.722-8.657A4.5 4.5 0 0013.5 6.5h-3A4.5 4.5 0 006 11c0 2.707 1.846 4.475 6 6.36V15.5z" />
|
||||
</svg>
|
||||
);
|
||||
if (count > 0) {
|
||||
return (
|
||||
<div
|
||||
className={`article-engagement-count comments-count${
|
||||
className ? ` ${className}` : ''
|
||||
}`}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="s"
|
||||
contentType="icon-left"
|
||||
url={`${articlePath}#comments`}
|
||||
icon={commentsSVG}
|
||||
tagName="a"
|
||||
>
|
||||
<a href={`${articlePath}#comments`}>
|
||||
<img src={icon} alt="chat" loading="lazy" />
|
||||
<span className="engagement-count-number">{count}</span>
|
||||
</a>
|
||||
</div>
|
||||
{count}
|
||||
<span className="hidden s:inline">
|
||||
comment
|
||||
{count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (count === 0) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="s"
|
||||
contentType="icon-left"
|
||||
url={`${articlePath}#comments`}
|
||||
icon={commentsSVG}
|
||||
tagName="a"
|
||||
>
|
||||
<span className="inline s:hidden">0</span>
|
||||
<span className="hidden s:inline">Add comment</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -22,14 +52,11 @@ export const CommentsCount = ({ count, articlePath, icon, className }) => {
|
|||
|
||||
CommentsCount.defaultProps = {
|
||||
count: 0,
|
||||
className: null,
|
||||
};
|
||||
|
||||
CommentsCount.propTypes = {
|
||||
count: PropTypes.number,
|
||||
articlePath: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
CommentsCount.displayName = 'CommentsCount';
|
||||
|
|
|
|||
63
app/javascript/articles/components/CommentsList.jsx
Normal file
63
app/javascript/articles/components/CommentsList.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
import { CommentListItem } from './CommentListItem';
|
||||
|
||||
const numberOfCommentsToShow = 2;
|
||||
|
||||
function linkToCommentsSection(articlePath) {
|
||||
const str = `${articlePath}#comments-container`;
|
||||
return str;
|
||||
}
|
||||
|
||||
function moreCommentsButton(comments, articlePath, totalCount) {
|
||||
let button = '';
|
||||
if (totalCount > numberOfCommentsToShow) {
|
||||
button = (
|
||||
<div className="crayons-story__comments__actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="s"
|
||||
tagName="a"
|
||||
url={linkToCommentsSection(articlePath)}
|
||||
>
|
||||
See all
|
||||
{' '}
|
||||
{totalCount}
|
||||
{' '}
|
||||
comments
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
export const CommentsList = ({ comments, articlePath, totalCount }) => {
|
||||
if (comments && comments.length > 0) {
|
||||
return (
|
||||
<div className="crayons-story__comments">
|
||||
{comments.slice(0, numberOfCommentsToShow).map((comment) => {
|
||||
return <CommentListItem comment={comment} />;
|
||||
})}
|
||||
|
||||
{moreCommentsButton(comments, articlePath, totalCount)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
CommentsList.displayName = 'CommentsList';
|
||||
|
||||
Comment.propTypes = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
published_at_int: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
CommentsList.propTypes = {
|
||||
comments: PropTypes.arrayOf(Comment.propTypes).isRequired,
|
||||
articlePath: PropTypes.string.isRequired,
|
||||
totalCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
|
@ -1,43 +1,38 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { articlePropTypes } from '../../src/components/common-prop-types';
|
||||
|
||||
export const ContentTitle = ({ article, currentTag }) => (
|
||||
<h3>
|
||||
{article.flare_tag && currentTag !== article.flare_tag.name && (
|
||||
<span
|
||||
className="tag-identifier"
|
||||
style={{
|
||||
background: article.flare_tag.bg_color_hex,
|
||||
color: article.flare_tag.text_color_hex,
|
||||
}}
|
||||
>
|
||||
{`#${article.flare_tag.name}`}
|
||||
</span>
|
||||
)}
|
||||
{article.class_name === 'PodcastEpisode' && (
|
||||
<span className="tag-identifier">podcast</span>
|
||||
)}
|
||||
{article.class_name === 'User' && (
|
||||
<span
|
||||
className="tag-identifier"
|
||||
style={{ background: '#5874d9', color: 'white' }}
|
||||
>
|
||||
person
|
||||
</span>
|
||||
)}
|
||||
{filterXSS(article.title)}
|
||||
</h3>
|
||||
export const ContentTitle = ({ article }) => (
|
||||
<h2 className="crayons-story__title">
|
||||
<a href={article.path} id={`article-link-${article.id}`}>
|
||||
{article.flare_tag && (
|
||||
<span
|
||||
className="crayons-story__flare-tag"
|
||||
style={{
|
||||
background: article.flare_tag.bg_color_hex,
|
||||
color: article.flare_tag.text_color_hex,
|
||||
}}
|
||||
>
|
||||
{`#${article.flare_tag.name}`}
|
||||
</span>
|
||||
)}
|
||||
{article.class_name === 'PodcastEpisode' && (
|
||||
<span className="crayons-story__flare-tag">podcast</span>
|
||||
)}
|
||||
{article.class_name === 'User' && (
|
||||
<span
|
||||
className="crayons-story__flare-tag"
|
||||
style={{ background: '#5874d9', color: 'white' }}
|
||||
>
|
||||
person
|
||||
</span>
|
||||
)}
|
||||
{filterXSS(article.title)}
|
||||
</a>
|
||||
</h2>
|
||||
);
|
||||
|
||||
ContentTitle.defaultProps = {
|
||||
currentTag: null,
|
||||
};
|
||||
|
||||
ContentTitle.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
currentTag: PropTypes.string,
|
||||
};
|
||||
|
||||
ContentTitle.displayName = 'ContentTitle';
|
||||
|
|
|
|||
92
app/javascript/articles/components/Meta.jsx
Normal file
92
app/javascript/articles/components/Meta.jsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { h } from 'preact';
|
||||
import {
|
||||
articlePropTypes,
|
||||
organizationPropType,
|
||||
} from '../../src/components/common-prop-types';
|
||||
import { PublishDate } from './PublishDate';
|
||||
|
||||
export const Meta = ({ article, organization }) => {
|
||||
const orgArticleIndexClassAbsent = !document.getElementById(
|
||||
'organization-article-index',
|
||||
);
|
||||
return (
|
||||
<div className="crayons-story__meta">
|
||||
<div className="crayons-story__author-pic">
|
||||
{organization && orgArticleIndexClassAbsent && (
|
||||
<a
|
||||
href={`/${organization.slug}`}
|
||||
className="crayons-logo crayons-logo--l"
|
||||
>
|
||||
<img
|
||||
alt={`${organization.name} logo`}
|
||||
src={organization.profile_image_90}
|
||||
className="crayons-logo__image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={`/${article.user.username}`}
|
||||
className={`crayons-avatar ${
|
||||
organization && orgArticleIndexClassAbsent
|
||||
? 'crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted'
|
||||
: 'crayons-avatar--l'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={article.user.profile_image_90}
|
||||
alt={`${article.user.username} profile`}
|
||||
className="crayons-avatar__image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<a
|
||||
href={`/${article.user.username}`}
|
||||
className="crayons-story__secondary fw-medium"
|
||||
>
|
||||
{filterXSS(
|
||||
article.class_name === 'User'
|
||||
? article.user.username
|
||||
: article.user.name,
|
||||
)}
|
||||
</a>
|
||||
{organization &&
|
||||
!document.getElementById('organization-article-index') && (
|
||||
<span>
|
||||
<span className="crayons-story__tertiary fw-normal">
|
||||
{' for '}
|
||||
</span>
|
||||
<a
|
||||
href={`/${organization.slug}`}
|
||||
className="crayons-story__secondary fw-medium"
|
||||
>
|
||||
{organization.name}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<a href={article.path} className="crayons-story__tertiary fs-xs">
|
||||
<PublishDate
|
||||
readablePublishDate={article.readable_publish_date}
|
||||
publishedTimestap={article.published_timestamp}
|
||||
publishedAtInt={article.published_at_int}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Meta.defaultProps = {
|
||||
organization: null,
|
||||
};
|
||||
|
||||
Meta.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
organization: organizationPropType,
|
||||
};
|
||||
|
||||
Meta.displayName = 'Meta';
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { organizationPropType } from '../../src/components/common-prop-types';
|
||||
|
||||
export const OrganizationHeadline = ({ organization }) => {
|
||||
if (organization && !document.getElementById('organization-article-index')) {
|
||||
return (
|
||||
<div className="article-organization-headline">
|
||||
<a className="org-headline-filler" href={`/${organization.slug}`}>
|
||||
<img
|
||||
alt={`${organization.name} logo`}
|
||||
src={organization.profile_image_90}
|
||||
loading="lazy"
|
||||
/>
|
||||
{organization.name}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
OrganizationHeadline.defaultProps = {
|
||||
organization: null,
|
||||
};
|
||||
|
||||
OrganizationHeadline.propTypes = {
|
||||
organization: organizationPropType,
|
||||
};
|
||||
|
||||
OrganizationHeadline.displayName = 'OrganizationHeadline';
|
||||
|
|
@ -1,21 +1,43 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const PublishDate = ({ readablePublishDate, publishedTimestamp }) => {
|
||||
if (publishedTimestamp) {
|
||||
return <time dateTime={publishedTimestamp}>{readablePublishDate}</time>;
|
||||
}
|
||||
/* global timeAgo */
|
||||
|
||||
return <time>{readablePublishDate}</time>;
|
||||
export const PublishDate = ({
|
||||
readablePublishDate,
|
||||
publishedTimestamp,
|
||||
publishedAtInt,
|
||||
}) => {
|
||||
const timeAgoIndicator = timeAgo({
|
||||
oldTimeInSeconds: publishedAtInt,
|
||||
formatter: (x) => x,
|
||||
maxDisplayedAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
const timeAgoText = () => {
|
||||
if (timeAgoIndicator) {
|
||||
return ` (${timeAgoIndicator})`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<time dateTime={publishedTimestamp}>
|
||||
{readablePublishDate}
|
||||
{timeAgoText()}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
|
||||
PublishDate.defaultProps = {
|
||||
publishedTimestamp: null,
|
||||
publishedAtInt: null,
|
||||
};
|
||||
|
||||
PublishDate.propTypes = {
|
||||
readablePublishDate: PropTypes.string.isRequired,
|
||||
publishedTimestamp: PropTypes.string,
|
||||
publishedAtInt: PropTypes.string,
|
||||
};
|
||||
|
||||
PublishDate.displayName = 'PublishDate';
|
||||
|
|
|
|||
|
|
@ -1,28 +1,40 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { articlePropTypes } from '../../src/components/common-prop-types';
|
||||
import { Button } from '../../crayons/Button';
|
||||
|
||||
export const ReactionsCount = ({ article, icon }) => {
|
||||
export const ReactionsCount = ({ article }) => {
|
||||
const totalReactions = article.positive_reactions_count || 0;
|
||||
const reactionsSVG = () => (
|
||||
<svg
|
||||
className="crayons-icon"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M18.884 12.595l.01.011L12 19.5l-6.894-6.894.01-.01A4.875 4.875 0 0112 5.73a4.875 4.875 0 016.884 6.865zM6.431 7.037a3.375 3.375 0 000 4.773L12 17.38l5.569-5.569a3.375 3.375 0 10-4.773-4.773L9.613 10.22l-1.06-1.062 2.371-2.372a3.375 3.375 0 00-4.492.25v.001z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="article-engagement-count reactions-count">
|
||||
<a href={article.path}>
|
||||
<img src={icon} alt="heart" loading="lazy" />
|
||||
<span
|
||||
id={`engagement-count-number-${article.id}`}
|
||||
className="engagement-count-number"
|
||||
>
|
||||
{totalReactions}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="s"
|
||||
contentType="icon-left"
|
||||
url={article.path}
|
||||
icon={reactionsSVG}
|
||||
tagName="a"
|
||||
>
|
||||
{totalReactions}
|
||||
<span className="hidden s:inline">
|
||||
reaction
|
||||
{totalReactions !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ReactionsCount.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ReactionsCount.displayName = 'ReactionsCount';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ReadingTime = ({ articlePath, readingTime }) => {
|
||||
export const ReadingTime = ({ readingTime }) => {
|
||||
// we have ` ... || null` for the case article.reading_time is undefined
|
||||
return (
|
||||
<a href={articlePath} className="article-reading-time">
|
||||
<small className="crayons-story__tertiary mr-2">
|
||||
{`${readingTime < 1 ? 1 : readingTime} min read`}
|
||||
</a>
|
||||
</small>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -15,7 +15,6 @@ ReadingTime.defaultProps = {
|
|||
};
|
||||
|
||||
ReadingTime.propTypes = {
|
||||
articlePath: PropTypes.string.isRequired,
|
||||
readingTime: PropTypes.number,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,39 +5,43 @@ import { articlePropTypes } from '../../src/components/common-prop-types';
|
|||
export class SaveButton extends Component {
|
||||
componentDidMount() {
|
||||
const { isBookmarked } = this.props;
|
||||
this.setState({ buttonText: isBookmarked ? 'SAVED' : 'SAVE' });
|
||||
this.setState({ buttonText: isBookmarked ? 'Saved' : 'Save' });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { buttonText } = this.state;
|
||||
|
||||
const { article, isBookmarked, onClick } = this.props;
|
||||
const mouseOut = _e => {
|
||||
this.setState({ buttonText: isBookmarked ? 'SAVED' : 'SAVE' });
|
||||
|
||||
const mouseOver = (_e) => {
|
||||
this.setState({ buttonText: isBookmarked ? 'Unsave' : 'Save' });
|
||||
};
|
||||
const mouseOver = _e => {
|
||||
if (isBookmarked) {
|
||||
this.setState({ buttonText: 'UNSAVE' });
|
||||
}
|
||||
|
||||
const mouseOut = (_e) => {
|
||||
this.setState({ buttonText: isBookmarked ? 'Saved' : 'Save' });
|
||||
};
|
||||
|
||||
const handleClick = (_e) => {
|
||||
onClick(_e);
|
||||
this.setState({ buttonText: isBookmarked ? 'Unsave' : 'Saved' });
|
||||
};
|
||||
|
||||
if (article.class_name === 'Article') {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`article-engagement-count engage-button bookmark-button ${
|
||||
isBookmarked ? 'selected' : ''
|
||||
className={`crayons-btn crayons-btn--s ${
|
||||
isBookmarked ? 'crayons-btn--ghost' : 'crayons-btn--secondary'
|
||||
}`}
|
||||
data-initial-feed
|
||||
data-reactable-id={article.id}
|
||||
onClick={onClick}
|
||||
onClick={handleClick}
|
||||
onMouseOver={mouseOver}
|
||||
onFocus={mouseOver}
|
||||
onMouseout={mouseOut}
|
||||
onBlur={mouseOut}
|
||||
>
|
||||
<span className={isBookmarked ? 'bm-success' : 'bm-initial'}>
|
||||
{buttonText}
|
||||
</span>
|
||||
{buttonText}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -45,8 +49,7 @@ export class SaveButton extends Component {
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
style={{ width: '122px' }}
|
||||
className="article-engagement-count engage-button follow-action-button"
|
||||
className="crayons-btn crayons-btn--secondary fs-s"
|
||||
data-info={`{"id":${article.id},"className":"User"}`}
|
||||
data-follow-action-button
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const SearchSnippet = ({ highlightText }) => {
|
|||
|
||||
if (bodyTextSnippet.length > 0) {
|
||||
return (
|
||||
<div className="search-snippet">
|
||||
<div className="crayons-story__snippet">
|
||||
<span>{bodyTextSnippet}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { tagPropTypes } from '../../src/components/common-prop-types';
|
||||
|
||||
export const TagList = ({ tags = [], className }) => (
|
||||
<div className={`tags${className ? ` ${className}` : ''}`}>
|
||||
export const TagList = ({ tags = [] }) => (
|
||||
<div className="crayons-story__tags">
|
||||
{tags.map(tag => (
|
||||
<a href={`/t/${tag}`}>
|
||||
<span className="tag">{`#${tag}`}</span>
|
||||
<a className="crayons-tag" href={`/t/${tag}`}>
|
||||
<span className="crayons-tag__prefix">#</span>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
TagList.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
TagList.propTypes = {
|
||||
tags: tagPropTypes.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
TagList.displayName = 'TagList';
|
||||
|
|
|
|||
22
app/javascript/articles/components/Video.jsx
Normal file
22
app/javascript/articles/components/Video.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { h } from 'preact';
|
||||
import { articlePropTypes } from '../../src/components/common-prop-types';
|
||||
|
||||
export const Video = ({article}) => {
|
||||
return (
|
||||
<a
|
||||
href={article.path}
|
||||
className="crayons-story__video"
|
||||
style={`background-image:url(${article.cloudinary_video_url})`}
|
||||
>
|
||||
<span className="crayons-story__video__time">
|
||||
{article.video_duration_in_minutes}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
Video.propTypes = {
|
||||
article: articlePropTypes.isRequired,
|
||||
};
|
||||
|
||||
Video.displayName = 'Video';
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { CommentsList } from '../CommentsList';
|
||||
import { singleComment, threeCommentList } from './utilities/commentUtilities';
|
||||
// import '../../../../assets/javascripts/utilities/timeAgo';
|
||||
|
||||
// Show buttons
|
||||
|
||||
// When 1 top comment and 1 total comment: hide button
|
||||
|
||||
// When 2 top comments and 2 total comments: hide button
|
||||
|
||||
// When more than 3 total comments: show button
|
||||
|
||||
describe('<CommentsList />', () => {
|
||||
it('should not render without comments', () => {
|
||||
const tree = render(<CommentsList />);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not render with empty comments array', () => {
|
||||
const tree = render(
|
||||
<CommentsList comments={[]} articlePath="" totalCount={0} />,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render comments', () => {
|
||||
global.timeAgo = jest.fn(() => '4 days ago');
|
||||
const tree = render(
|
||||
<CommentsList
|
||||
comments={threeCommentList}
|
||||
articlePath=""
|
||||
totalCount={3}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not render "See all comments" button when there is a single comment and it is the top comment', () => {
|
||||
const tree = render(
|
||||
<CommentsList comments={[singleComment]} articlePath="" totalCount={1} />,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not render "See all comments" button when there are two comments and are both top comments', () => {
|
||||
const tree = render(
|
||||
<CommentsList
|
||||
comments={[singleComment, singleComment]}
|
||||
articlePath=""
|
||||
totalCount={2}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render "See all comments" button when there are two top comments and more than two total', () => {
|
||||
const tree = render(
|
||||
<CommentsList
|
||||
comments={[singleComment, singleComment]}
|
||||
articlePath=""
|
||||
totalCount={3}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<CommentsList /> should not render "See all comments" button when there are two comments and are both top comments 1`] = `
|
||||
<div
|
||||
class="crayons-story__comments"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/cedrick_effertz"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png"
|
||||
/>
|
||||
</span>
|
||||
Cedrick Effertz
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/cedrick_effertz/comment/5"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Waistcoat goth vice salvia.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/cedrick_effertz"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png"
|
||||
/>
|
||||
</span>
|
||||
Cedrick Effertz
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/cedrick_effertz/comment/5"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Waistcoat goth vice salvia.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<CommentsList /> should not render "See all comments" button when there is a single comment and it is the top comment 1`] = `
|
||||
<div
|
||||
class="crayons-story__comments"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/cedrick_effertz"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png"
|
||||
/>
|
||||
</span>
|
||||
Cedrick Effertz
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/cedrick_effertz/comment/5"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Waistcoat goth vice salvia.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<CommentsList /> should not render with empty comments array 1`] = `""`;
|
||||
|
||||
exports[`<CommentsList /> should not render without comments 1`] = `""`;
|
||||
|
||||
exports[`<CommentsList /> should render "See all comments" button when there are two top comments and more than two total 1`] = `
|
||||
<div
|
||||
class="crayons-story__comments"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/cedrick_effertz"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png"
|
||||
/>
|
||||
</span>
|
||||
Cedrick Effertz
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/cedrick_effertz/comment/5"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Waistcoat goth vice salvia.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/cedrick_effertz"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png"
|
||||
/>
|
||||
</span>
|
||||
Cedrick Effertz
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/cedrick_effertz/comment/5"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Waistcoat goth vice salvia.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-story__comments__actions"
|
||||
>
|
||||
<a
|
||||
class="crayons-btn crayons-btn--secondary crayons-btn--s"
|
||||
href="#comments-container"
|
||||
>
|
||||
See all 3 comments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<CommentsList /> should render comments 1`] = `
|
||||
<div
|
||||
class="crayons-story__comments"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/cedrick_effertz"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png"
|
||||
/>
|
||||
</span>
|
||||
Cedrick Effertz
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/cedrick_effertz/comment/5"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Waistcoat goth vice salvia.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment pl-2 cursor-pointer"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="crayons-comment__meta"
|
||||
>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
href="/miss_shavon_langosh"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar mr-2"
|
||||
>
|
||||
<img
|
||||
alt="{comment.username} avatar"
|
||||
class="crayons-avatar__image"
|
||||
src="/uploads/user/profile_image/7/8a6cd3cf-2b23-4500-b265-00f0b524e4de.png"
|
||||
/>
|
||||
</span>
|
||||
Miss Shavon Langosh
|
||||
</a>
|
||||
<a
|
||||
class="crayons-story__tertiary ml-1"
|
||||
href="/miss_shavon_langosh/comment/k"
|
||||
>
|
||||
<time>
|
||||
4 days ago
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-comment__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Twee chartreuse kitsch. Before they sold out 8-bit semiotics vhs pitchfork 3 wolf moon. Park migas retro lumbersexual goth whatever slow-carb selvage.</p>
|
||||
|
||||
",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-story__comments__actions"
|
||||
>
|
||||
<a
|
||||
class="crayons-btn crayons-btn--secondary crayons-btn--s"
|
||||
href="#comments-container"
|
||||
>
|
||||
See all 3 comments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
export const singleComment = {
|
||||
comment_id: 5,
|
||||
user_id: 5,
|
||||
published_timestamp: '2020-04-28T01:08:11Z',
|
||||
published_at_int: 1588036091,
|
||||
safe_processed_html: '<p>Waistcoat goth vice salvia.</p>\n\n',
|
||||
path: '/cedrick_effertz/comment/5',
|
||||
username: 'cedrick_effertz',
|
||||
name: 'Cedrick Effertz',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png',
|
||||
};
|
||||
|
||||
export const threeCommentList = [
|
||||
{
|
||||
comment_id: 5,
|
||||
user_id: 5,
|
||||
published_timestamp: '2020-04-28T01:08:11Z',
|
||||
published_at_int: 1588036091,
|
||||
safe_processed_html: '<p>Waistcoat goth vice salvia.</p>\n\n',
|
||||
path: '/cedrick_effertz/comment/5',
|
||||
username: 'cedrick_effertz',
|
||||
name: 'Cedrick Effertz',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/5/6210af11-9bdc-4903-8023-bf199a733427.png',
|
||||
},
|
||||
{
|
||||
comment_id: 20,
|
||||
user_id: 7,
|
||||
published_timestamp: '2020-04-28T01:08:12Z',
|
||||
published_at_int: 1588036092,
|
||||
safe_processed_html:
|
||||
'<p>Twee chartreuse kitsch. Before they sold out 8-bit semiotics vhs pitchfork 3 wolf moon. Park migas retro lumbersexual goth whatever slow-carb selvage.</p>\n\n',
|
||||
path: '/miss_shavon_langosh/comment/k',
|
||||
username: 'miss_shavon_langosh',
|
||||
name: 'Miss Shavon Langosh',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/7/8a6cd3cf-2b23-4500-b265-00f0b524e4de.png',
|
||||
},
|
||||
{
|
||||
comment_id: 29,
|
||||
user_id: 10,
|
||||
published_timestamp: '2020-04-28T01:08:12Z',
|
||||
published_at_int: 1588036092,
|
||||
safe_processed_html:
|
||||
'<p>Put a bird on it squid pickled williamsburg farm-to-table keytar yr keffiyeh. Gluten-free bespoke tumblr everyday intelligentsia kombucha. Single-origin coffee chambray occupy tattooed echo mustache cray.</p>\n\n',
|
||||
path: '/brady_orn/comment/13',
|
||||
username: 'brady_orn',
|
||||
name: 'Brady Orn',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/10/5f86a870-3d5a-42c3-8d92-3c8e2c0c7094.png',
|
||||
},
|
||||
];
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
export * from './ArticleCoverImage';
|
||||
export * from './ContentTitle';
|
||||
export * from './CommentsCount';
|
||||
export * from './OrganizationHeadline';
|
||||
export * from './CommentsList';
|
||||
export * from './Meta';
|
||||
export * from './PublishDate';
|
||||
export * from './ReactionsCount';
|
||||
export * from './ReadingTime';
|
||||
export * from './SaveButton';
|
||||
export * from './SearchSnippet';
|
||||
export * from './ReadingTime';
|
||||
export * from './TagList';
|
||||
export * from './Video';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './Article';
|
||||
export * from './FeaturedArticle';
|
||||
export * from './LoadingArticle';
|
||||
export * from './PodcastArticle';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { h, render } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Article, FeaturedArticle, LoadingArticle } from '../articles';
|
||||
import { Article, LoadingArticle } from '../articles';
|
||||
import { Feed } from '../articles/Feed';
|
||||
import { TodaysPodcasts, PodcastEpisode } from '../podcasts';
|
||||
import { articlePropTypes } from '../src/components/common-prop-types';
|
||||
|
|
@ -30,7 +30,7 @@ function sendFeaturedArticleAnalytics(articleId) {
|
|||
|
||||
const FeedLoading = () => (
|
||||
<div>
|
||||
<LoadingArticle version='featured' />
|
||||
<LoadingArticle version="featured" />
|
||||
<LoadingArticle />
|
||||
<LoadingArticle />
|
||||
<LoadingArticle />
|
||||
|
|
@ -42,7 +42,7 @@ const FeedLoading = () => (
|
|||
|
||||
const PodcastEpisodes = ({ episodes }) => (
|
||||
<TodaysPodcasts>
|
||||
{episodes.map(episode => (
|
||||
{episodes.map((episode) => (
|
||||
<PodcastEpisode episode={episode} />
|
||||
))}
|
||||
</TodaysPodcasts>
|
||||
|
|
@ -59,7 +59,7 @@ PodcastEpisodes.propTypes = {
|
|||
/**
|
||||
* Renders the main feed.
|
||||
*/
|
||||
export const renderFeed = timeFrame => {
|
||||
export const renderFeed = (timeFrame) => {
|
||||
const feedContainer = document.getElementById('homepage-feed');
|
||||
|
||||
render(
|
||||
|
|
@ -67,7 +67,6 @@ export const renderFeed = timeFrame => {
|
|||
timeFrame={timeFrame}
|
||||
renderFeed={({
|
||||
feedItems,
|
||||
feedIcons,
|
||||
podcastEpisodes,
|
||||
bookmarkedFeedItems,
|
||||
bookmarkClick,
|
||||
|
|
@ -78,8 +77,6 @@ export const renderFeed = timeFrame => {
|
|||
}
|
||||
|
||||
const commonProps = {
|
||||
reactionsIcon: feedIcons.REACTIONS_ICON,
|
||||
commentsIcon: feedIcons.COMMENTS_ICON,
|
||||
bookmarkClick,
|
||||
};
|
||||
|
||||
|
|
@ -92,15 +89,16 @@ export const renderFeed = timeFrame => {
|
|||
// 3. Rest of the stories for the feed
|
||||
return (
|
||||
<div>
|
||||
<FeaturedArticle
|
||||
<Article
|
||||
{...commonProps}
|
||||
article={featuredStory}
|
||||
isFeatured
|
||||
isBookmarked={bookmarkedFeedItems.has(featuredStory.id)}
|
||||
/>
|
||||
{podcastEpisodes.length > 0 && (
|
||||
<PodcastEpisodes episodes={podcastEpisodes} />
|
||||
)}
|
||||
{(subStories || []).map(story => (
|
||||
{(subStories || []).map((story) => (
|
||||
<Article
|
||||
{...commonProps}
|
||||
article={story}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,25 @@ import { articlePropTypes } from '../src/components/common-prop-types';
|
|||
|
||||
export const PodcastEpisode = ({ episode }) => {
|
||||
return (
|
||||
<a
|
||||
className="individual-podcast-link"
|
||||
href={`/${episode.podcast.slug}/${episode.slug}`}
|
||||
>
|
||||
<img src={episode.podcast.image_90} alt={episode.title} />
|
||||
<div className="individual-podcast-link-details">
|
||||
<strong>{episode.title}</strong>
|
||||
{' '}
|
||||
{episode.podcast.title}
|
||||
<div className="crayons-podcast-episode">
|
||||
<a
|
||||
href={`/${episode.podcast.slug}/${episode.slug}`}
|
||||
className="crayons-podcast-episode__cover"
|
||||
>
|
||||
<img src={episode.podcast.image_90} alt={episode.title} />
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<p className="crayons-podcast-episode__author">
|
||||
{episode.podcast.title}
|
||||
</p>
|
||||
<h2 className="crayons-story__title mb-0">
|
||||
<a href={`/${episode.podcast.slug}/${episode.slug}`}>
|
||||
{episode.title}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { h } from 'preact';
|
|||
import { defaultChildrenPropTypes } from '../src/components/common-prop-types';
|
||||
|
||||
export const TodaysPodcasts = ({ children }) => (
|
||||
<div className="single-article single-article-podcast-div">
|
||||
<h3>
|
||||
<a href="/pod">Today's Podcasts</a>
|
||||
</h3>
|
||||
{children}
|
||||
<div className="crayons-story">
|
||||
<div class="crayons-story__body">
|
||||
<h3 class="crayons-story__headline"><a href="/pod">Today's Podcasts</a></h3>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,33 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PodcastEpisode /> component should render a podcast episode 1`] = `
|
||||
<a
|
||||
class="individual-podcast-link"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
<div
|
||||
class="crayons-podcast-episode"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
<div
|
||||
class="individual-podcast-link-details"
|
||||
<a
|
||||
class="crayons-podcast-episode__cover"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
<strong>
|
||||
monitor recontextualize
|
||||
</strong>
|
||||
Rubber local
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
</a>
|
||||
<div>
|
||||
<p
|
||||
class="crayons-podcast-episode__author"
|
||||
>
|
||||
Rubber local
|
||||
</p>
|
||||
<h2
|
||||
class="crayons-story__title mb-0"
|
||||
>
|
||||
<a
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
monitor recontextualize
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,65 +2,107 @@
|
|||
|
||||
exports[`<TodaysPodcasts /> component should render a today's podcasts 1`] = `
|
||||
<div
|
||||
class="single-article single-article-podcast-div"
|
||||
class="crayons-story"
|
||||
>
|
||||
<h3>
|
||||
<a
|
||||
href="/pod"
|
||||
>
|
||||
Today's Podcasts
|
||||
</a>
|
||||
</h3>
|
||||
<a
|
||||
class="individual-podcast-link"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
<div
|
||||
class="crayons-story__body"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
<div
|
||||
class="individual-podcast-link-details"
|
||||
<h3
|
||||
class="crayons-story__headline"
|
||||
>
|
||||
<strong>
|
||||
monitor recontextualize
|
||||
</strong>
|
||||
Rubber local
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="individual-podcast-link"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
<a
|
||||
href="/pod"
|
||||
>
|
||||
Today's Podcasts
|
||||
</a>
|
||||
</h3>
|
||||
<div
|
||||
class="individual-podcast-link-details"
|
||||
class="crayons-podcast-episode"
|
||||
>
|
||||
<strong>
|
||||
monitor recontextualize
|
||||
</strong>
|
||||
Rubber local
|
||||
<a
|
||||
class="crayons-podcast-episode__cover"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
</a>
|
||||
<div>
|
||||
<p
|
||||
class="crayons-podcast-episode__author"
|
||||
>
|
||||
Rubber local
|
||||
</p>
|
||||
<h2
|
||||
class="crayons-story__title mb-0"
|
||||
>
|
||||
<a
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
monitor recontextualize
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="individual-podcast-link"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
<div
|
||||
class="individual-podcast-link-details"
|
||||
class="crayons-podcast-episode"
|
||||
>
|
||||
<strong>
|
||||
monitor recontextualize
|
||||
</strong>
|
||||
Rubber local
|
||||
<a
|
||||
class="crayons-podcast-episode__cover"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
</a>
|
||||
<div>
|
||||
<p
|
||||
class="crayons-podcast-episode__author"
|
||||
>
|
||||
Rubber local
|
||||
</p>
|
||||
<h2
|
||||
class="crayons-story__title mb-0"
|
||||
>
|
||||
<a
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
monitor recontextualize
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
class="crayons-podcast-episode"
|
||||
>
|
||||
<a
|
||||
class="crayons-podcast-episode__cover"
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
<img
|
||||
alt="monitor recontextualize"
|
||||
src="/images/16.png"
|
||||
/>
|
||||
</a>
|
||||
<div>
|
||||
<p
|
||||
class="crayons-podcast-episode__author"
|
||||
>
|
||||
Rubber local
|
||||
</p>
|
||||
<h2
|
||||
class="crayons-story__title mb-0"
|
||||
>
|
||||
<a
|
||||
href="/monitor-recontextualize/undefined"
|
||||
>
|
||||
monitor recontextualize
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -32,4 +32,5 @@ export const articlePropTypes = PropTypes.shape({
|
|||
positive_reactions_count: PropTypes.number,
|
||||
reactions_count: PropTypes.number,
|
||||
comments_count: PropTypes.number,
|
||||
reading_time: PropTypes.number,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,65 +1,92 @@
|
|||
<div class="single-article single-article-small-pic" data-content-user-id="<%= story.user_id %>">
|
||||
<div class="crayons-story <% if featured == true %>crayons-story--featured<% end %>" data-content-user-id="<%= story.user_id %>">
|
||||
<% if featured == true %>
|
||||
<div id="featured-story-marker" data-featured-article="articles-<%= story.id %>"></div>
|
||||
<a href="<%= story.path %>" class="crayons-story__cover" data-featured-article="articles-<%= story.id %>">
|
||||
<div style="background-color: <%= story.main_image_background_hex_color %>; background-image: url(<%= cloud_cover_url(story.main_image) %>);" class="crayons-story__cover__image"></div>
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
<% if story.video.present? && story.video_thumbnail_url.present? %>
|
||||
<a class="single-article-video-preview" style="background-image:url(<%= story.cloudinary_video_url %>)" href="<%= story.path %>">
|
||||
<div class="single-article-video-duration">
|
||||
<img src="<%= asset_path("video-camera.svg") %>" alt="video camera"><%= story.video_duration_in_minutes %></div>
|
||||
<a href="<%= story.path %>" class="crayons-story__video" style="background-image:url(<%= story.cloudinary_video_url %>)">
|
||||
<span class="crayons-story__video__time">
|
||||
<%= story.video_duration_in_minutes %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if story.cached_organization && !@organization_article_index %>
|
||||
<div class="article-organization-headline">
|
||||
<a class="org-headline-filler" href="/<%= story.cached_organization.slug %>">
|
||||
<img alt="<%= story.cached_organization.name %> logo" src="<%= story.cached_organization.profile_image_90 %>"><%= story.cached_organization.name %>
|
||||
</a>
|
||||
|
||||
<div class="crayons-story__body">
|
||||
<div class="crayons-story__top">
|
||||
<div class="crayons-story__meta">
|
||||
<div class="crayons-story__author-pic">
|
||||
<% if story.cached_organization && !@organization_article_index %>
|
||||
<a class="crayons-logo crayons-logo--l" href="/<%= story.cached_organization.slug %>">
|
||||
<img alt="<%= story.cached_organization.name %> logo" src="<%= story.cached_organization.profile_image_90 %>" class="crayons-logo__image">
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
<a href="/<%= story.cached_user.username %>" class="crayons-avatar <% if story.cached_organization && !@organization_article_index %> crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted <% else %> crayons-avatar--l <% end %> ">
|
||||
<img src="<%= ProfileImage.new(story.cached_user).get(width: 90) %>" alt="<%= story.cached_user.username %> profile" class="crayons-avatar__image" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<a href="/<%= story.cached_user.username %>" class="crayons-story__secondary fw-medium">
|
||||
<%= story.cached_user.name %>
|
||||
<%= render "shared/pro_checkmark" if story.cached_user.pro %>
|
||||
</a>
|
||||
<% if story.cached_organization && !@organization_article_index %>
|
||||
<span>
|
||||
<span class="crayons-story__tertiary fw-normal"> for </span>
|
||||
<a href="/<%= story.cached_organization.slug %>" class="crayons-story__secondary fw-medium"><%= story.cached_organization.name %></a>
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<a href="<%= story.path %>" class="crayons-story__tertiary fs-xs"><time datetime="<%= story.published_timestamp %>"><%= story.readable_publish_date %></time><span class="time-ago-indicator-initial-placeholder" data-seconds="<%= story.published_at_int %>"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<a href="/<%= story.cached_user.username %>" class="small-pic-link-wrapper">
|
||||
<div class="small-pic">
|
||||
<img src="<%= ProfileImage.new(story.cached_user).get(width: 90) %>" alt="<%= story.cached_user.username %> profile" width="40" height="40" />
|
||||
|
||||
<div class="crayons-story__indention">
|
||||
<h2 class="crayons-story__title">
|
||||
<a href="<%= story.path %>" data-preload-image="<%= cloud_cover_url(story.main_image) %>" id="article-link-<%= story.id %>">
|
||||
<%= render "articles/tag_identifier", story: story, tag: @tag %>
|
||||
</a>
|
||||
</h2>
|
||||
<div class="crayons-story__tags">
|
||||
<% story.cached_tag_list_array.each do |tag| %>
|
||||
<a href="/t/<%= tag %>" class="crayons-tag"><span class="crayons-tag__prefix">#</span><%= tag %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="crayons-story__bottom">
|
||||
<div class="crayons-story__details">
|
||||
<a href="<%= story.path %>" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left" data-reaction-count data-reactable-id="<%= story.id %>">
|
||||
<%= inline_svg_tag("small-heart.svg", aria: true, width: 24, height: 24, class: "crayons-icon", title: "Reactions") %>
|
||||
<%= story.positive_reactions_count %>
|
||||
<span class="hidden s:inline"> reactions</span>
|
||||
</a>
|
||||
<% if story.comments_count > 0 %>
|
||||
<a href="<%= story.path %>#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left">
|
||||
<%= inline_svg_tag("small-comment.svg", aria: true, width: 24, height: 24, class: "crayons-icon", title: "Comments") %>
|
||||
<%= story.comments_count %>
|
||||
<span class="hidden s:inline"> comments</span>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="crayons-story__save">
|
||||
<small class="crayons-story__tertiary fs-xs mr-2">
|
||||
<%= story.reading_time < 1 ? 1 : story.reading_time %> min read
|
||||
</small>
|
||||
<button
|
||||
type="button"
|
||||
class="crayons-btn crayons-btn--secondary crayons-btn--s bookmark-button"
|
||||
data-reactable-id="<%= story.id %>"
|
||||
aria-label="Save to reading list"
|
||||
title="Save to reading list">
|
||||
<span class="bm-initial">Save</span>
|
||||
<span class="bm-success">Saved</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<%= story.path %>" class="small-pic-link-wrapper index-article-link" data-preload-image="<%= cloud_cover_url(story.main_image) %>" id="article-link-<%= story.id %>">
|
||||
<div class="content">
|
||||
<h3>
|
||||
<%= render "articles/tag_identifier", story: story, tag: @tag %>
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
<h4>
|
||||
<a href="/<%= story.cached_user.username %>">
|
||||
<%= story.cached_user.name %><%= render "shared/pro_checkmark" if story.cached_user.pro %>・<time datetime="<%= story.published_timestamp %>"><%= story.readable_publish_date %></time><span class="time-ago-indicator-initial-placeholder" data-seconds="<%= story.published_at_int %>"></span>
|
||||
</a>
|
||||
</h4>
|
||||
<div class="tags">
|
||||
<% story.cached_tag_list_array.each do |tag| %>
|
||||
<a href="/t/<%= tag %>"><span class="tag">#<%= tag %></span></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="article-engagement-count reactions-count" data-reaction-count data-reactable-id="<%= story.id %>">
|
||||
<a href="<%= story.path %>">
|
||||
<img src="<%= asset_path("reactions-stack.png") %>" alt="Reactions" /><span id="engagement-count-number-<%= story.id %>" class="engagement-count-number">
|
||||
<%= story.positive_reactions_count %>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<% if story.comments_count > 0 %>
|
||||
<div class="article-engagement-count comments-count">
|
||||
<a href="<%= story.path %>#comments">
|
||||
<img src="<%= asset_path("comments-bubble.png") %>" alt="chat" /><span class="engagement-count-number"><%= story.comments_count %></span>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<a
|
||||
href="<%= story.path %>"
|
||||
class="article-reading-time">
|
||||
<%= story.reading_time < 1 ? 1 : story.reading_time %> min read
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="article-engagement-count engage-button bookmark-button"
|
||||
data-reactable-id="<%= story.id %>"
|
||||
aria-label="Save to reading list"
|
||||
title="Save to reading list">
|
||||
<span class="bm-initial">SAVE</span>
|
||||
<span class="bm-success">SAVED</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<% flare_tag = FlareTag.new(story, @tag).tag %>
|
||||
<% if flare_tag %>
|
||||
<span class="tag-identifier black-identifier" style="background:<%= flare_tag.bg_color_hex %>;color:<%= flare_tag.text_color_hex %>">
|
||||
<span class="crayons-story__flare-tag" style="background:<%= flare_tag.bg_color_hex %>;color:<%= flare_tag.text_color_hex %>">
|
||||
#<%= flare_tag.name %></span><% end %><%= story.title %>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<% @stories.each_with_index do |story, i| %>
|
||||
<%= render "articles/single_story", story: story %>
|
||||
<%= render "articles/single_story", story: story, featured: false %>
|
||||
<% end %>
|
||||
<% if @stories.size > 1 %>
|
||||
<div class="placeholder-div"></div>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<div class="classified-filters">
|
||||
<div class="classified-filters-categories">
|
||||
<a href="<%= classified_listings_path %>" class="<%= "selected" if params[:category].blank? %> data-no-instant="true">all</a>
|
||||
<a href="<%= classified_listings_path %>" class="<%= "selected" if params[:category].blank? %> data-no-instant=" true ">all</a>
|
||||
<% categories_for_display.each do |cat| %>
|
||||
<a href="<%= classified_listing_category_path(category: cat[:slug]) %>" class="<%= "selected" if params[:category] == cat[:slug] %> data-no-instant="true"><%= cat[:name] %></a>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div id="organization-article-index"></div>
|
||||
<% @stories.each_with_index do |story, i| %>
|
||||
<%= render "articles/single_story", story: story %>
|
||||
<%= render "articles/single_story", story: story, featured: false %>
|
||||
<% end %>
|
||||
<% if @stories.size > 1 %>
|
||||
<div class="placeholder-div"></div>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,5 @@
|
|||
<% if @featured_story.id %>
|
||||
<div id="featured-story-marker" data-featured-article="articles-<%= @featured_story.id %>"></div>
|
||||
<img src="<%= cloud_cover_url(@featured_story.main_image) %>" style="display:none" alt="<%= @featured_story.title %>" />
|
||||
<a href="<%= @featured_story.path %>" id="article-link-<%= @featured_story.id %>" class="index-article-link" aria-label="Main Story" data-featured-article="articles-<%= @featured_story.id %>">
|
||||
<div class="single-article big-article" data-content-user-id="<%= @featured_story.user_id %>">
|
||||
<div class="picture image-final" style="background-color:<%= @featured_story.main_image_background_hex_color %>;background-image:url(<%= cloud_cover_url(@featured_story.main_image) %>)"></div>
|
||||
<div class="content-wrapper">
|
||||
<h3>
|
||||
<%= @featured_story.title %>
|
||||
</h3>
|
||||
</div>
|
||||
<a href="/<%= @featured_story.cached_user.username %>" class="featured-profile-button">
|
||||
<img class="featured-profile-pic" src="<%= ProfileImage.new(@featured_story.cached_user).get(width: 90) %>" width="40" height="40" alt="<%= @featured_story.title %>" />
|
||||
</a>
|
||||
<div class="featured-user-name">
|
||||
<a href="/<%= @featured_story.cached_user.username %>">
|
||||
<%= @featured_story.cached_user.name %><%= render "shared/pro_checkmark" if @featured_story.cached_user.pro %>・<time datetime="<%= @featured_story.published_timestamp %>"><%= @featured_story.readable_publish_date %></time>
|
||||
<span class="time-ago-indicator-initial-placeholder" data-seconds="<%= @featured_story.published_at_int %>"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="featured-tags tags">
|
||||
<% @featured_story.cached_tag_list_array.each do |tag| %>
|
||||
<a href="/t/<%= tag %>"><span class="tag">#<%= tag %></span></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @featured_story.comments_count > 0 %>
|
||||
<div class="article-engagement-count comments-count featured-engagement-count">
|
||||
<a href="<%= @featured_story.path %>#comments"><img src="<%= asset_path("comments-bubble.png") %>" alt="chat" />
|
||||
<span class="engagement-count-number"><%= @featured_story.comments_count %></span>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @featured_story.positive_reactions_count > 0 %>
|
||||
<div class="article-engagement-count reactions-count featured-engagement-count" data-reaction-count data-reactable-id="<%= @featured_story.id %>">
|
||||
<a href="<%= @featured_story.path %>"><img src="<%= asset_path("reactions-stack.png") %>" alt="heart" /><span id="engagement-count-number-<%= @featured_story.id %>" class="engagement-count-number">
|
||||
<%= @featured_story.positive_reactions_count %>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<a
|
||||
href="<%= @featured_story.path %>"
|
||||
class="article-reading-time">
|
||||
<%= @featured_story.reading_time < 1 ? 1 : @featured_story.reading_time %> min read
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="article-engagement-count engage-button bookmark-button featured-engagement-count"
|
||||
data-reactable-id="<%= @featured_story.id %>"
|
||||
aria-label="Save to reading list"
|
||||
title="Save to reading list">
|
||||
<span class="bm-initial">SAVE</span>
|
||||
<span class="bm-success">SAVED</span>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
<%= render "articles/single_story", story: @featured_story, featured: true %>
|
||||
<% end %>
|
||||
|
||||
<div id="article-index-podcast-div"></div>
|
||||
|
|
@ -65,7 +11,7 @@
|
|||
<% if !user_signed_in? && i == 4 %>
|
||||
<%= render "stories/sign_in_invitation" %>
|
||||
<% end %>
|
||||
<%= render "articles/single_story", story: story %>
|
||||
<%= render "articles/single_story", story: story, featured: false %>
|
||||
<% end %>
|
||||
<% if @stories.size > 1 %>
|
||||
<div class="placeholder-div"></div>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@
|
|||
var clickedEl = event.target;
|
||||
if (hasClass(clickedEl, "bm-initial") || hasClass(clickedEl, "bm-success")) {
|
||||
//do nothing
|
||||
} else if (hasClass(clickedEl.parentNode, "single-article")) {
|
||||
} else if (hasClass(clickedEl.parentNode, "crayons-story")) {
|
||||
clickedEl.parentNode.classList.add('active');
|
||||
} else if (hasClass(clickedEl.parentNode.parentNode, "single-article")) {
|
||||
} else if (hasClass(clickedEl.parentNode.parentNode, "crayons-story")) {
|
||||
clickedEl.parentNode.parentNode.classList.add('active');
|
||||
} else if (hasClass(clickedEl.parentNode.parentNode.parentNode, "single-article")) {
|
||||
} else if (hasClass(clickedEl.parentNode.parentNode.parentNode, "crayons-story")) {
|
||||
clickedEl.parentNode.parentNode.parentNode.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ json.array!(@stories) do |article|
|
|||
json.extract! article, *article_methods_to_include
|
||||
|
||||
json.top_comments article.top_comments do |comment|
|
||||
comment = comment.decorate
|
||||
json.comment_id comment.id
|
||||
json.extract! comment, :body_markdown, :updated_at, :user_id
|
||||
json.extract! comment, :user_id, :published_timestamp, :published_at_int, :safe_processed_html, :path
|
||||
json.extract! comment.user, :username, :name, :profile_image_90
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
<div class="pinned-articles">
|
||||
<h5><img src="<%= asset_path("pushpin.svg") %>" width="20" height="20" alt="pin" /> Pinned</h5>
|
||||
<% @pinned_stories.each do |story| %>
|
||||
<%= render "articles/single_story", story: story %>
|
||||
<%= render "articles/single_story", story: story, featured: false %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% @stories.each_with_index do |story, i| %>
|
||||
<%= render "articles/single_story", story: story %>
|
||||
<%= render "articles/single_story", story: story, featured: false %>
|
||||
<% if i == 0 && @comments.present? %>
|
||||
<%= render "users/comments_section" %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ platform.
|
|||
certain elements of content, etc. If you were to host the current version of
|
||||
the app, you would be inadvertently impersonating the core dev.to project.
|
||||
- DEV has commercial licenses for certain design components as described in our
|
||||
[Design License
|
||||
Info](https://docs.dev.to/design/branding/#design-license-info) section. These
|
||||
licenses are specific to the dev.to project, and may not extend to self-hosted
|
||||
versions.
|
||||
[Design License Info](https://docs.dev.to/design/branding/#design-license-info)
|
||||
section. These licenses are specific to the dev.to project, and may not extend
|
||||
to self-hosted versions.
|
||||
|
||||
### I want to stand up my own entity using the DEV codebase, how can I help?
|
||||
|
||||
|
|
@ -38,5 +37,5 @@ platform.
|
|||
|
||||
The long-term benefits of doing this the right way far outweigh the short-term
|
||||
gains of spinning something up without consulting us. If you are interested in
|
||||
getting started with this process, let us know in [this
|
||||
thread](https://dev.to/ben/so-you-want-to-stand-up-your-own-instance-of-dev-help-thread-1elo).
|
||||
getting started with this process, let us know in
|
||||
[this thread](https://dev.to/ben/so-you-want-to-stand-up-your-own-instance-of-dev-help-thread-1elo).
|
||||
|
|
|
|||
|
|
@ -46,4 +46,13 @@ RSpec.describe CommentDecorator, type: :decorator do
|
|||
expect(comment.published_timestamp).to eq(comment.created_at.utc.iso8601)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#published_at_int" do
|
||||
let(:comment) { create(:comment) }
|
||||
|
||||
it "returns the creation date as an integer" do
|
||||
expect(comment.created_at).not_to be_nil
|
||||
expect(comment.decorate.published_at_int).to eq(comment.created_at.to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ RSpec.describe "User visits articles by tag", type: :system do
|
|||
end
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article", count: 2)
|
||||
expect(page).to have_selector(".crayons-story", count: 2)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/week" }
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 1)
|
||||
expect(page).to have_selector(".crayons-story", visible: :visible, count: 2)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -32,11 +32,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/month" }
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 2)
|
||||
expect(page).to have_selector(".crayons-story", visible: :visible, count: 3)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -52,11 +52,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/year" }
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 3)
|
||||
expect(page).to have_selector(".crayons-story", visible: :visible, count: 4)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -73,12 +73,12 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/infinity" }
|
||||
|
||||
it "shows correct articles and cta count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 5)
|
||||
expect(page).to have_selector(".crayons-story", visible: :visible, count: 5)
|
||||
expect(page).to have_selector(".feed-cta", count: 1)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -96,12 +96,12 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/latest" }
|
||||
|
||||
it "shows correct articles and cta count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 5)
|
||||
expect(page).to have_selector(".crayons-story", visible: :visible, count: 5)
|
||||
expect(page).to have_selector(".feed-cta", count: 1)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -125,11 +125,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
end
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 1)
|
||||
expect(page).to have_xpath("//article[contains(@class, 'crayons-story') and contains(@class, 'false')]", count: 1)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -143,11 +143,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/month" }
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 2)
|
||||
expect(page).to have_xpath("//article[contains(@class, 'crayons-story') and contains(@class, 'false')]", count: 2)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -163,11 +163,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/year" }
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 3)
|
||||
expect(page).to have_xpath("//article[contains(@class, 'crayons-story') and contains(@class, 'false')]", count: 3)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -184,11 +184,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/top/infinity" }
|
||||
|
||||
it "shows correct articles count" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 4)
|
||||
expect(page).to have_xpath("//article[contains(@class, 'crayons-story') and contains(@class, 'false')]", count: 4)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
@ -206,11 +206,11 @@ RSpec.describe "User visits articles by timeframe", type: :system do
|
|||
before { visit "/latest" }
|
||||
|
||||
it "shows correct articles" do
|
||||
expect(page).to have_selector(".single-article-small-pic", count: 4)
|
||||
expect(page).to have_xpath("//article[contains(@class, 'crayons-story') and contains(@class, 'false')]", count: 4)
|
||||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible, count: 1)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible, count: 1)
|
||||
end
|
||||
|
||||
it "shows the correct articles" do
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ RSpec.describe "User visits a homepage", type: :system do
|
|||
end
|
||||
|
||||
it "shows the main article" do
|
||||
expect(page).to have_selector(".big-article", visible: :visible)
|
||||
expect(page).to have_selector(".crayons-story--featured", visible: :visible)
|
||||
end
|
||||
|
||||
it "shows the main article readable date", js: true do
|
||||
expect(page).to have_selector(".big-article time", text: "Mar 4")
|
||||
expect(page).to have_selector(".crayons-story--featured time", text: "Mar 4")
|
||||
end
|
||||
|
||||
it "embeds the main article published timestamp" do
|
||||
selector = ".big-article time[datetime='#{timestamp}']"
|
||||
selector = ".crayons-story--featured time[datetime='#{timestamp}']"
|
||||
expect(page).to have_selector(selector)
|
||||
end
|
||||
end
|
||||
|
|
@ -35,17 +35,17 @@ RSpec.describe "User visits a homepage", type: :system do
|
|||
end
|
||||
|
||||
it "shows correct articles " do
|
||||
expect(page).to have_selector(".single-article", count: 2)
|
||||
expect(page).to have_selector(".crayons-story", count: 2)
|
||||
expect(page).to have_text(article.title)
|
||||
expect(page).to have_text(article2.title)
|
||||
end
|
||||
|
||||
it "shows all articles dates", js: true do
|
||||
expect(page).to have_selector(".single-article time", text: "Mar 4", count: 2)
|
||||
expect(page).to have_selector(".crayons-story time", text: "Mar 4", count: 2)
|
||||
end
|
||||
|
||||
it "embeds all articles published timestamps" do
|
||||
selector = ".single-article time[datetime='#{timestamp}']"
|
||||
selector = ".crayons-story time[datetime='#{timestamp}']"
|
||||
expect(page).to have_selector(selector, count: 2)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ RSpec.describe "Organization index", type: :system do
|
|||
end
|
||||
|
||||
it "shows articles" do
|
||||
expect(page).to have_selector("div.single-article", count: 2)
|
||||
expect(page).to have_selector("div.crayons-story", count: 2)
|
||||
end
|
||||
|
||||
it "shows the sidebar" do
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ RSpec.describe "Display articles search spec", type: :system, js: true, elastics
|
|||
|
||||
expect(page).to have_content(found_article_one.title)
|
||||
expect(find("#article-link-#{found_article_one.id}")["href"]).to include(found_article_one.path)
|
||||
expect(find_button("SAVE")["data-reactable-id"].to_i).to eq(found_article_one.id)
|
||||
expect(find_link("5 min read")).to be_present
|
||||
expect(page).to have_selector("button[data-reactable-id=\"#{found_article_one.id}\"]")
|
||||
expect(page).to have_content("5 min read")
|
||||
expect(find_link("#ruby")["href"]).to include("/t/ruby")
|
||||
expect(find_link(found_article_one.user.name)["href"]).to include(found_article_one.username)
|
||||
expect(find(".engagement-count-number").text.to_i).to eq(2)
|
||||
expect(page).to have_content("0 reactions")
|
||||
expect(page).to have_content("2 comments")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ RSpec.describe "Display users search spec", type: :system, js: true, elasticsear
|
|||
visit "/search?q=jane&filters=class_name:User"
|
||||
|
||||
expect(page).to have_content(found_user.name)
|
||||
expect(find(".tag-identifier").text).to eq("person")
|
||||
expect(find("span.crayons-story__flare-tag").text).to have_content("person")
|
||||
expect(find(:xpath, "//img[@alt='#{found_user.username} profile']")["src"]).to include(found_user.profile_image_90)
|
||||
expect(JSON.parse(find_button("+ FOLLOW")["data-info"])["id"]).to eq(found_user.id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RSpec.describe "User index", type: :system do
|
|||
end
|
||||
|
||||
it "shows user's articles" do
|
||||
within(".single-article") do
|
||||
within(".crayons-story") do
|
||||
expect(page).to have_content(article.title)
|
||||
expect(page).not_to have_content(other_article.title)
|
||||
end
|
||||
|
|
@ -85,7 +85,7 @@ RSpec.describe "User index", type: :system do
|
|||
end
|
||||
|
||||
it "shows user's articles" do
|
||||
within(".single-article") do
|
||||
within(".crayons-story") do
|
||||
expect(page).to have_content(article.title)
|
||||
expect(page).not_to have_content(other_article.title)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue