Pro: History (#3220)
* Add base history support * Add tags and ranking and make it work * Fix styling * Add article title and text excerpt to the index * Add pagination support * Debounce the search to 300ms * Show history only to pro users * Use routing helpers instead of hardcoding * Raise the page size to 100 * Remove console log * A bit of regrouping
This commit is contained in:
parent
9e616de9f8
commit
2bf54edb9c
19 changed files with 682 additions and 35 deletions
|
|
@ -63,12 +63,15 @@ function handleFormSubmit(e) {
|
|||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(data);
|
||||
|
||||
xhr.onload = function() {
|
||||
xhr.onload = function onload() {
|
||||
var article = form.closest('div.single-article');
|
||||
|
||||
if (xhr.status === 200) {
|
||||
onXhrSuccess(form, article, values);
|
||||
var message = values.commit === 'Mute Notifications' ? 'Notifications Muted' : 'Notifications Restored';
|
||||
var message =
|
||||
values.commit === 'Mute Notifications'
|
||||
? 'Notifications Muted'
|
||||
: 'Notifications Restored';
|
||||
article.querySelector('.dashboard-meta-details').innerHTML = message;
|
||||
} else {
|
||||
article.querySelector('.dashboard-meta-details').innerHTML =
|
||||
|
|
@ -141,9 +144,10 @@ function initializeEllipsisMenuToggle() {
|
|||
}
|
||||
|
||||
// Hide ellipsis menus when you click outside of the ellipsis menu parent div
|
||||
document
|
||||
.getElementsByTagName('BODY')[0]
|
||||
.addEventListener('click', hideEllipsisMenus);
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
if (body) {
|
||||
body.addEventListener('click', hideEllipsisMenus);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeEllipsisMenu() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
function checkUserLoggedIn() {
|
||||
return document.getElementsByTagName('body')[0].getAttribute('data-user-status') == "logged-in"
|
||||
}
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
if (!body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return body.getAttribute('data-user-status') === 'logged-in';
|
||||
}
|
||||
|
|
|
|||
237
app/assets/stylesheets/history.scss
Normal file
237
app/assets/stylesheets/history.scss
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.history-container {
|
||||
@media screen and (min-width: 950px) {
|
||||
margin-top: -25px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.history-filters {
|
||||
width: 300px;
|
||||
float: left;
|
||||
min-height: 356px;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 0px;
|
||||
padding: 5px;
|
||||
@include themeable(
|
||||
border,
|
||||
theme-container-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
border-radius: 3px;
|
||||
font-size: 1em;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 4px;
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
lighten($light-gray, 2%)
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
}
|
||||
|
||||
.history-tags {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.history-tag {
|
||||
padding: 5px 0px;
|
||||
position: relative;
|
||||
padding-left: 5px;
|
||||
@include themeable(color, theme-color, $black);
|
||||
&:hover {
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background-hover,
|
||||
$light-gray
|
||||
);
|
||||
}
|
||||
&.selected {
|
||||
background: $purple;
|
||||
}
|
||||
}
|
||||
|
||||
.history-view-toggle {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 15px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.history-results {
|
||||
width: calc(100% - 20px);
|
||||
margin: 10px auto;
|
||||
margin-left: 10px;
|
||||
float: left;
|
||||
@include themeable(background, theme-container-background, white);
|
||||
@include themeable(
|
||||
border,
|
||||
theme-container-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
border-radius: 3px;
|
||||
min-height: 350px;
|
||||
display: none;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
width: calc(100% - 332px);
|
||||
margin-left: 20px;
|
||||
margin-top: 0px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
&.history-results--loaded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.history-results-header {
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
darken($light-gray, 2%)
|
||||
);
|
||||
padding: 17px;
|
||||
font-size: 1.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.history-item-wrapper {
|
||||
position: relative;
|
||||
.history-archive-butt {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
border: 1px solid $black;
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
@include themeable(border-color, theme-secondary-color, $medium-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.history-item {
|
||||
@include themeable(color, theme-color, $black);
|
||||
font-size: 0.8em;
|
||||
padding: 17px 17px 25px;
|
||||
@include themeable(
|
||||
border-bottom,
|
||||
theme-container-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
|
||||
.history-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.7em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.history-item-details {
|
||||
a {
|
||||
display: inline-block;
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
&.history-item-user {
|
||||
margin-right: 5px;
|
||||
img {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
border-radius: 100px;
|
||||
vertical-align: -5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.history-item-tag-collection {
|
||||
display: inline-block;
|
||||
}
|
||||
&.history-item-tag {
|
||||
margin-left: 1px;
|
||||
padding: 1px 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background-hover,
|
||||
$light-gray
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.history-empty {
|
||||
text-align: center;
|
||||
padding: 100px 0px;
|
||||
span.highlight {
|
||||
background: $bold-blue;
|
||||
border-radius: 3px;
|
||||
padding: 3px 8px;
|
||||
margin: 0px 8px;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
span[role='img'] {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-results-load-more {
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
@include themeable(border, theme-border, 1px solid $light-medium-gray);
|
||||
font-size: 17px;
|
||||
padding: 14px 5px;
|
||||
margin: 30px auto;
|
||||
width: 120px;
|
||||
max-width: 80%;
|
||||
border-radius: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 949px) {
|
||||
.history-home {
|
||||
.side-bar {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: calc(100% - 15px);
|
||||
margin: 10px auto;
|
||||
margin-left: 10px;
|
||||
margin-bottom: -15px;
|
||||
.history-filters {
|
||||
height: 85px;
|
||||
min-height: 85px;
|
||||
.history-tags {
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
.history-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,9 @@ body {
|
|||
}
|
||||
|
||||
.zen-mode {
|
||||
.top-bar, .primary-sticky-nav, .article-actions {
|
||||
.top-bar,
|
||||
.primary-sticky-nav,
|
||||
.article-actions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +59,7 @@ body {
|
|||
&.stories-search,
|
||||
&.podcast_episodes-index,
|
||||
&.reading_list_items-index,
|
||||
&.history-index,
|
||||
.tags-index,
|
||||
&.twitch_live_streams-show {
|
||||
margin-top: 68px;
|
||||
|
|
@ -136,7 +139,11 @@ input[type='email'] {
|
|||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
@include themeable(background, theme-container-accent-background, $light-purple);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
$light-purple
|
||||
);
|
||||
@include themeable(border, container-border, 2px solid $purple);
|
||||
padding: 12px 30px;
|
||||
z-index: 20;
|
||||
|
|
@ -149,12 +156,19 @@ input[type='email'] {
|
|||
-webkit-animation-name: slide-in-from-left;
|
||||
-webkit-animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-in-from-left {
|
||||
0% { left: -300px;}
|
||||
50%{ left : 0px;}
|
||||
90%{ left : 24px;}
|
||||
100%{ left: 20px;}
|
||||
0% {
|
||||
left: -300px;
|
||||
}
|
||||
50% {
|
||||
left: 0px;
|
||||
}
|
||||
90% {
|
||||
left: 24px;
|
||||
}
|
||||
100% {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
app/controllers/history_controller.rb
Normal file
16
app/controllers/history_controller.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class HistoryController < ApplicationController
|
||||
before_action :generate_algolia_search_key
|
||||
|
||||
def index
|
||||
authorize current_user, :pro_user?
|
||||
@history_index = true # used exclusively by the ERb templates
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_algolia_search_key
|
||||
params = { filters: "viewable_by:#{current_user.id}" }
|
||||
key = ApplicationConfig["ALGOLIASEARCH_SEARCH_ONLY_KEY"]
|
||||
@secured_algolia_key = Algolia.generate_secured_api_key(key, params)
|
||||
end
|
||||
end
|
||||
|
|
@ -14,6 +14,8 @@ class ReadingListItemsController < ApplicationController
|
|||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_algolia_search_key
|
||||
params = { filters: "viewable_by:#{current_user.id}" }
|
||||
@secured_algolia_key = Algolia.generate_secured_api_key(
|
||||
|
|
|
|||
227
app/javascript/history/history.jsx
Normal file
227
app/javascript/history/history.jsx
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { h, Component } from 'preact';
|
||||
import { PropTypes } from 'preact-compat';
|
||||
import debounce from 'lodash.debounce';
|
||||
import setupAlgoliaIndex from '../src/utils/algolia';
|
||||
|
||||
export class History extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { availableTags } = this.props;
|
||||
this.state = {
|
||||
query: '',
|
||||
items: [],
|
||||
totalCount: 0,
|
||||
index: null,
|
||||
itemsLoaded: false,
|
||||
hitsPerPage: 100,
|
||||
availableTags,
|
||||
selectedTags: [],
|
||||
page: 0,
|
||||
showNextPageButton: false,
|
||||
};
|
||||
|
||||
this.handleTyping = debounce(this.handleTyping.bind(this), 300, {
|
||||
leading: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const index = setupAlgoliaIndex({
|
||||
containerId: 'history',
|
||||
indexName: 'UserHistory',
|
||||
});
|
||||
|
||||
// get default result set from Algolia
|
||||
const { hitsPerPage } = this.state;
|
||||
index.search('', { hitsPerPage }).then(content => {
|
||||
this.setState({
|
||||
items: content.hits,
|
||||
totalCount: content.nbHits,
|
||||
index,
|
||||
itemsLoaded: true,
|
||||
showNextPageButton: content.hits.length === hitsPerPage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleTyping = event => {
|
||||
const query = event.target.value;
|
||||
const { selectedTags } = this.state;
|
||||
|
||||
this.setState({ page: 0, items: [] });
|
||||
this.search(query, { tags: selectedTags });
|
||||
};
|
||||
|
||||
toggleTag = (event, tag) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { query, selectedTags } = this.state;
|
||||
const newTags = selectedTags;
|
||||
if (newTags.indexOf(tag) === -1) {
|
||||
newTags.push(tag);
|
||||
} else {
|
||||
newTags.splice(newTags.indexOf(tag), 1);
|
||||
}
|
||||
|
||||
this.setState({ selectedTags: newTags, page: 0, items: [] });
|
||||
this.search(query, { tags: newTags });
|
||||
};
|
||||
|
||||
loadNextPage = () => {
|
||||
const { query, selectedTags, page } = this.state;
|
||||
this.setState({ page: page + 1 });
|
||||
this.search(query, { selectedTags });
|
||||
};
|
||||
|
||||
search(query, { tags }) {
|
||||
const { index, hitsPerPage, page, items } = this.state;
|
||||
const filters = { hitsPerPage, page };
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
filters.tagFilters = tags;
|
||||
}
|
||||
|
||||
index.search(query, filters).then(content => {
|
||||
// add new items to the bottom
|
||||
const allItems = items;
|
||||
const itemsIds = items.map(i => i.objectID);
|
||||
content.hits.forEach(item => {
|
||||
if (!itemsIds.includes(item.objectID)) {
|
||||
allItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
query,
|
||||
items: allItems,
|
||||
totalCount: content.nbHits,
|
||||
showNextPageButton: content.hits.length === hitsPerPage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderNoItems() {
|
||||
const { selectedTags, query } = this.state;
|
||||
|
||||
return (
|
||||
<div className="history-empty">
|
||||
<h1>
|
||||
{selectedTags.length === 0 && query.length === 0
|
||||
? 'Your History is Lonely'
|
||||
: 'Nothing with this filter 🤔'}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
const { items, itemsLoaded } = this.state;
|
||||
|
||||
if (items.length === 0 && itemsLoaded) {
|
||||
return this.renderNoItems();
|
||||
}
|
||||
|
||||
return items.map(item => (
|
||||
<div className="history-item-wrapper">
|
||||
<a className="history-item" href={item.article_path}>
|
||||
<div className="history-item-title">{item.article_title}</div>
|
||||
|
||||
<div className="history-item-details">
|
||||
<a
|
||||
className="history-item-user"
|
||||
href={`/${item.article_user.username}`}
|
||||
>
|
||||
<img src={item.article_user.profile_image_90} alt="Profile Pic" />
|
||||
{item.article_user.name}
|
||||
・
|
||||
{item.article_reading_time}
|
||||
{' '}
|
||||
min read・
|
||||
{`visited on ${item.readable_visited_at}`}
|
||||
・
|
||||
</a>
|
||||
<span className="history-item-tag-collection">
|
||||
{item.article_tags.map(tag => (
|
||||
<a className="history-item-tag" href={`/t/${tag}`}>
|
||||
#
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
renderTags() {
|
||||
const { availableTags, selectedTags } = this.state;
|
||||
|
||||
return availableTags.map(tag => (
|
||||
<a
|
||||
className={`history-tag ${
|
||||
selectedTags.indexOf(tag) > -1 ? 'selected' : ''
|
||||
}`}
|
||||
href={`/t/${tag}`}
|
||||
data-no-instant
|
||||
onClick={e => this.toggleTag(e, tag)}
|
||||
>
|
||||
#
|
||||
{tag}
|
||||
</a>
|
||||
));
|
||||
}
|
||||
|
||||
renderNextPageButton() {
|
||||
const { showNextPageButton } = this.state;
|
||||
|
||||
if (showNextPageButton) {
|
||||
return (
|
||||
<div className="history-results-load-more">
|
||||
<button onClick={e => this.loadNextPage(e)} type="button">
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { itemsLoaded, totalCount } = this.state;
|
||||
|
||||
const allItems = this.renderItems();
|
||||
const allTags = this.renderTags();
|
||||
const nextPageButton = this.renderNextPageButton();
|
||||
|
||||
return (
|
||||
<div className="home history-home">
|
||||
<div className="side-bar">
|
||||
<div className="widget history-filters">
|
||||
<input onKeyUp={this.handleTyping} placeHolder="search your list" />
|
||||
<div className="history-tags">{allTags}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`history-results ${
|
||||
itemsLoaded ? 'history-results--loaded' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="history-results-header">
|
||||
History
|
||||
{' '}
|
||||
{`(${totalCount})`}
|
||||
</div>
|
||||
<div>{allItems}</div>
|
||||
{nextPageButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
History.propTypes = {
|
||||
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
22
app/javascript/packs/history.jsx
Normal file
22
app/javascript/packs/history.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { h, render } from 'preact';
|
||||
import { getUserDataAndCsrfToken } from '../chat/util';
|
||||
import { History } from '../history/history';
|
||||
|
||||
function loadComponent() {
|
||||
getUserDataAndCsrfToken().then(({ currentUser }) => {
|
||||
const root = document.getElementById('history');
|
||||
if (root) {
|
||||
render(
|
||||
<History availableTags={currentUser.followed_tag_names} />,
|
||||
root,
|
||||
root.firstElementChild,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.InstantClick.on('change', () => {
|
||||
loadComponent();
|
||||
});
|
||||
|
||||
loadComponent();
|
||||
|
|
@ -128,13 +128,19 @@ export class ReadingList extends Component {
|
|||
src={item.reactable_user.profile_image_90}
|
||||
alt="Profile Pic"
|
||||
/>
|
||||
{item.reactable_user.name}・{item.reactable_published_date}・
|
||||
{item.reading_time} min read・
|
||||
{item.reactable_user.name}
|
||||
・
|
||||
{item.reactable_published_date}
|
||||
・
|
||||
{item.reading_time}
|
||||
{' '}
|
||||
min read・
|
||||
</a>
|
||||
<span className="readinglist-item-tag-collection">
|
||||
{item.reactable_tags.map(tag => (
|
||||
<a className="readinglist-item-tag" href={`/t/${tag}`}>
|
||||
#{tag}
|
||||
#
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</span>
|
||||
|
|
@ -160,9 +166,9 @@ export class ReadingList extends Component {
|
|||
</h1>
|
||||
<h3>
|
||||
Hit the
|
||||
<span class="highlight">SAVE</span>
|
||||
<span className="highlight">SAVE</span>
|
||||
or
|
||||
<span class="highlight">
|
||||
<span className="highlight">
|
||||
Bookmark
|
||||
<span role="img" aria-label="Bookmark">
|
||||
🔖
|
||||
|
|
@ -193,7 +199,8 @@ export class ReadingList extends Component {
|
|||
data-no-instant
|
||||
onClick={e => this.toggleTag(e, tag)}
|
||||
>
|
||||
#{tag}
|
||||
#
|
||||
{tag}
|
||||
</a>
|
||||
));
|
||||
const snackBar = archiving ? (
|
||||
|
|
|
|||
7
app/javascript/src/utils/algolia.js
Normal file
7
app/javascript/src/utils/algolia.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function setupAlgoliaIndex({ containerId, indexName }) {
|
||||
const id = document.querySelector("meta[name='algolia-public-id']").content;
|
||||
const key = document.getElementById(containerId).dataset.algoliaKey;
|
||||
const env = document.querySelector("meta[name='environment']").content;
|
||||
const client = algoliasearch(id, key);
|
||||
return client.initIndex(`${indexName}_${env}`);
|
||||
}
|
||||
|
|
@ -1,4 +1,61 @@
|
|||
class PageView < ApplicationRecord
|
||||
include AlgoliaSearch
|
||||
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :article
|
||||
|
||||
algoliasearch index_name: "UserHistory", per_environment: true, if: :belongs_to_pro_user? do
|
||||
attributes :referrer, :time_tracked_in_seconds, :user_agent, :article_tags
|
||||
|
||||
attribute(:article_title) { article.title }
|
||||
attribute(:article_path) { article.path }
|
||||
attribute(:article_reading_time) { article.reading_time }
|
||||
attribute(:viewable_by) { user_id }
|
||||
attribute(:visited_at_timestamp) { created_at.to_i }
|
||||
|
||||
attribute :article_user do
|
||||
user = article.user
|
||||
{
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
profile_image_90: user.profile_image_90
|
||||
}
|
||||
end
|
||||
|
||||
attribute :readable_visited_at do
|
||||
if created_at.year == Time.current.year
|
||||
created_at.strftime("%b %e")
|
||||
else
|
||||
created_at.strftime("%b %e '%y")
|
||||
end
|
||||
end
|
||||
|
||||
searchableAttributes(
|
||||
%i[referrer user_agent article_title article_searchable_tags article_searchable_text],
|
||||
)
|
||||
|
||||
tags { article_tags }
|
||||
|
||||
attributesForFaceting ["filterOnly(viewable_by)"]
|
||||
|
||||
customRanking ["desc(visited_at_timestamp)"]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def belongs_to_pro_user?
|
||||
user&.pro?
|
||||
end
|
||||
|
||||
def article_searchable_tags
|
||||
article.cached_tag_list
|
||||
end
|
||||
|
||||
def article_searchable_text
|
||||
article.body_text[0..350]
|
||||
end
|
||||
|
||||
def article_tags
|
||||
article.decorate.cached_tag_list_array
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,17 +19,17 @@
|
|||
navigation
|
||||
</div>
|
||||
<div class="sidebar-nav-element">
|
||||
<a class="sidebar-nav-link" href="/readinglist?we-hope-you-like-the-new-and-improved-reading-list---more-positive-changes-coming=woohoo">
|
||||
<a class="sidebar-nav-link" href="<%= readinglist_path %>">
|
||||
<img class="nav-emoji" src="<%= asset_path("emoji/emoji-one-bookmark.png") %>" alt="reading list icon" /> Reading List
|
||||
<span id="reading-list-count"></span>
|
||||
</a>
|
||||
<a class="sidebar-nav-link" href="/listings">
|
||||
<a class="sidebar-nav-link" href="<%= classified_listings_path %>">
|
||||
<img class="nav-emoji" src="<%= asset_path("emoji/emoji-one-clipboard.png") %>" alt="listings icon" /> Listings
|
||||
</a>
|
||||
<a class="sidebar-nav-link" href="/videos">
|
||||
<a class="sidebar-nav-link" href="<%= videos_path %>">
|
||||
<img class="nav-emoji" src="<%= asset_path("emoji/emoji-one-videocamera.png") %>" alt="videos icon" /> Videos
|
||||
</a>
|
||||
<a class="sidebar-nav-link" href="/pod">
|
||||
<a class="sidebar-nav-link" href="<%= pod_path %>">
|
||||
<img class="nav-emoji" src="<%= asset_path("emoji/emoji-one-headphones.png") %>" alt="podcasts icon" /> Podcasts
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
<% end %>
|
||||
<% Tag.where(supported: true).order("hotness_score DESC").limit(30).pluck(:id, :name).each do |tag_array| %>
|
||||
<div class="sidebar-nav-element" id="default-sidebar-element-<%= tag_array.second %>">
|
||||
<a class="sidebar-nav-link" href="/t/<%= tag_array.second %>">
|
||||
<a class="sidebar-nav-link" href="<%= tag_path(tag_array.second) %>">
|
||||
<span class="sidebar-nav-tag-text">
|
||||
#<%= tag_array.second %>
|
||||
</span>
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="sidebar-nav-subheader">
|
||||
<a href="/tags">View all-time top tags</a>
|
||||
<a href="<%= tags_path %>">View all-time top tags</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
26
app/views/history/index.html.erb
Normal file
26
app/views/history/index.html.erb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<% title "History" %>
|
||||
|
||||
<%= content_for :page_meta do %>
|
||||
<% page_url = "#{ApplicationConfig["APP_PROTOCOL"]}#{ApplicationConfig["APP_DOMAIN"]}#{history_path}" %>
|
||||
<link rel="canonical" href="<%= page_url %>" />
|
||||
<meta name="description" content="Page views history for <%= community_qualified_name %>">
|
||||
<meta name="keywords" content="software development,engineering,rails,javascript,ruby">
|
||||
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="<%= page_url %>" />
|
||||
<meta property="og:title" content="History - <%= community_qualified_name %>" />
|
||||
<meta property="og:description" content="My Posts History on <%= community_qualified_name %>" />
|
||||
<meta property="og:site_name" content="<%= community_qualified_name %>" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@<%= ApplicationConfig["SITE_TWITTER_HANDLE"] %>">
|
||||
<meta name="twitter:title" content="History - <%= community_qualified_name %>">
|
||||
<meta name="twitter:description" content="Page views history for <%= community_qualified_name %>">
|
||||
<% end %>
|
||||
|
||||
<div class="home">
|
||||
<div class="history-container" id="history" data-algolia-key="<%= @secured_algolia_key %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= javascript_pack_tag "history", defer: true %>
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
<div id="footer-container" class="container <%= "centered-footer" unless current_page.include?("stories-show") %>">
|
||||
<div class="inner-footer-container">
|
||||
<a href="/">Home</a> <a href="/about">About</a> <a href="/privacy">Privacy Policy</a>
|
||||
<a href="/terms">Terms of Use</a> <a href="/contact">Contact</a> <a href="/code-of-conduct">Code of Conduct</a> <br/>
|
||||
<a href="/terms">Terms of Use</a> <a href="/contact">Contact</a> <a href="/code-of-conduct">Code of Conduct</a><br/>
|
||||
<%= ApplicationConfig["COMMUNITY_NAME"] %> Community copyright 2016 - <%= Time.current.year %> 🔥
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<%= javascript_include_tag "hello-dev.js", defer: true %>
|
||||
<%# javascript_include_tag "hello-dev.js", defer: true %>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<% cache "base_inline_styles_#{@story_show}_#{@article_index}_#{@home_page}_#{@article_show}_#{view_class}_#{@notifications_index}_#{core_pages?}_#{@tags_index}_#{@reading_list_items_index}_#{ApplicationConfig['DEPLOYMENT_SIGNATURE']}_#{user_signed_in?}", expires_in: 8.hours do %>
|
||||
<% cache "base_inline_styles_#{@story_show}_#{@article_index}_#{@home_page}_#{@article_show}_#{view_class}_#{@notifications_index}_#{core_pages?}_#{@tags_index}_#{@reading_list_items_index}_#{@history_index}_#{ApplicationConfig['DEPLOYMENT_SIGNATURE']}_#{user_signed_in?}", expires_in: 8.hours do %>
|
||||
<% if @story_show %>
|
||||
<style>
|
||||
<% Rails.application.config.assets.compile = true %>
|
||||
|
|
@ -34,11 +34,12 @@
|
|||
<%= Rails.application.assets["preact/sidebar-widget.css"].to_s.html_safe %>
|
||||
<% end %>
|
||||
</style>
|
||||
<% elsif @notifications_index || @reading_list_items_index %>
|
||||
<% elsif @notifications_index || @reading_list_items_index || @history_index %>
|
||||
<style>
|
||||
<% Rails.application.config.assets.compile = true %>
|
||||
<%= Rails.application.assets["scaffolds.css"].to_s.html_safe %>
|
||||
<%= Rails.application.assets["readinglist.css"].to_s.html_safe %>
|
||||
<%= Rails.application.assets["history.css"].to_s.html_safe %>
|
||||
<%= Rails.application.assets["articles.css"].to_s.html_safe %>
|
||||
<%= Rails.application.assets["comments.css"].to_s.html_safe %>
|
||||
<%= Rails.application.assets["notifications.css"].to_s.html_safe %>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Rails.application.routes.draw do
|
|||
post "save_status"
|
||||
end
|
||||
end
|
||||
resources :tags, only: %i[index show edit update]
|
||||
resources :tags, only: %i[index edit update]
|
||||
resources :users, only: %i[index show edit update] do
|
||||
member do
|
||||
post "banish"
|
||||
|
|
@ -298,6 +298,7 @@ Rails.application.routes.draw do
|
|||
get "/pod" => "podcast_episodes#index"
|
||||
get "/readinglist" => "reading_list_items#index"
|
||||
get "/readinglist/:view" => "reading_list_items#index", constraints: { view: /archive/ }
|
||||
get "/history", to: "history#index", as: :history
|
||||
|
||||
get "/feed" => "articles#feed", as: "feed", defaults: { format: "rss" }
|
||||
get "/feed/tag/:tag" => "articles#feed",
|
||||
|
|
@ -307,7 +308,7 @@ Rails.application.routes.draw do
|
|||
get "/rss" => "articles#feed", defaults: { format: "rss" }
|
||||
|
||||
get "/tag/:tag" => "stories#index"
|
||||
get "/t/:tag" => "stories#index"
|
||||
get "/t/:tag", to: "stories#index", as: :tag
|
||||
get "/t/:tag/edit", to: "tags#edit"
|
||||
get "/t/:tag/admin", to: "tags#admin"
|
||||
patch "/tag/:id", to: "tags#update"
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write",
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
],
|
||||
"{app,spec}/**/*.rb": [
|
||||
|
|
@ -85,15 +85,16 @@
|
|||
"webpack-dev-server": "^2.11.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/clipboard-copy-element": "^1.0.1",
|
||||
"@rails/webpacker": "^3.5.5",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-preact": "^1.1.0",
|
||||
"chart.js": "^2.8.0",
|
||||
"codemirror": "^5.47.0",
|
||||
"focus-visible": "^4.1.5",
|
||||
"@github/clipboard-copy-element": "^1.0.1",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"linkstate": "^1.1.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "^0.6.2",
|
||||
"preact": "^8.4.2",
|
||||
"preact-compat": "^3.19.0",
|
||||
|
|
|
|||
20
spec/requests/history_spec.rb
Normal file
20
spec/requests/history_spec.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "History", type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let(:pro_user) { create(:user, :pro) }
|
||||
|
||||
describe "GET /history" do
|
||||
it "does not allow access to a regular user" do
|
||||
sign_in user
|
||||
expect { get history_path }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
|
||||
it "allows access to a pro user" do
|
||||
sign_in pro_user
|
||||
get history_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include("History")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7012,7 +7012,7 @@ lodash.clonedeep@^4.3.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
lodash.debounce@4.0.8, lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue