[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:
Josh Puetz 2020-05-12 07:57:57 -05:00 committed by GitHub
parent 7cd38e2c46
commit 2abe830377
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 4066 additions and 2871 deletions

View 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

View 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

View 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

View 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

View 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

View file

@ -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 {

View file

@ -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');

View file

@ -1,4 +1,4 @@
'use strict';
/**
* This script hunts for podcast's "Record" for both the podcast_episde's

View file

@ -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'),
);
});
}

View file

@ -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">&nbsp;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">&nbsp;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>\
&nbsp;\
</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>';
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -24,6 +24,7 @@
border-radius: $radius;
width: 100%;
height: 100%;
display: inline-block;
vertical-align: bottom; // todo: do we need it?
}

View 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);
}
}

View 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;
}
}

View 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;
}
}
}
}

View 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);
}
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -10,4 +10,8 @@ class CommentDecorator < ApplicationDecorator
created_at.utc.iso8601
end
def published_at_int
created_at.to_i
end
end

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,

View file

@ -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">&nbsp;</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%' }}>
&nbsp;
</h4>
<div className="tags single-article-loading" />
</div>
);
};

View file

@ -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')}

View file

@ -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' };

View file

@ -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',
};

View file

@ -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

View file

@ -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();
});
});

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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,
};

View 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';

View 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,
};

View file

@ -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">
&nbsp;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';

View 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,
};

View file

@ -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';

View 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';

View file

@ -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';

View file

@ -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';

View file

@ -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">
&nbsp;reaction
{totalReactions !== 1 ? 's' : ''}
</span>
</Button>
);
};
ReactionsCount.propTypes = {
article: articlePropTypes.isRequired,
icon: PropTypes.string.isRequired,
};
ReactionsCount.displayName = 'ReactionsCount';

View file

@ -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,
};

View file

@ -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
>

View file

@ -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>
);

View file

@ -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';

View 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';

View file

@ -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();
});
});

View file

@ -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>
`;

View file

@ -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',
},
];

View file

@ -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';

View file

@ -1,4 +1,3 @@
export * from './Article';
export * from './FeaturedArticle';
export * from './LoadingArticle';
export * from './PodcastArticle';

View file

@ -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}

View file

@ -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>
);
};

View file

@ -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&apos;s Podcasts</a>
</h3>
{children}
<div className="crayons-story">
<div class="crayons-story__body">
<h3 class="crayons-story__headline"><a href="/pod">Today&apos;s Podcasts</a></h3>
{children}
</div>
</div>
);

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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,
});

View file

@ -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">&nbsp;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">&nbsp;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>

View file

@ -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 %>

View file

@ -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>

View file

@ -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 %>

View file

@ -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>

View file

@ -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>

View file

@ -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');
}
});

View file

@ -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

View file

@ -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 %>

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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