diff --git a/app/assets/javascripts/initializers/initializeEllipsisMenu.js b/app/assets/javascripts/initializers/initializeEllipsisMenu.js index 0f60a12d0..5e1e0f5fe 100644 --- a/app/assets/javascripts/initializers/initializeEllipsisMenu.js +++ b/app/assets/javascripts/initializers/initializeEllipsisMenu.js @@ -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() { diff --git a/app/assets/javascripts/utilities/checkUserLoggedIn.js b/app/assets/javascripts/utilities/checkUserLoggedIn.js index 93aa3cea7..1bd909af6 100644 --- a/app/assets/javascripts/utilities/checkUserLoggedIn.js +++ b/app/assets/javascripts/utilities/checkUserLoggedIn.js @@ -1,3 +1,8 @@ function checkUserLoggedIn() { - return document.getElementsByTagName('body')[0].getAttribute('data-user-status') == "logged-in" -} \ No newline at end of file + const body = document.getElementsByTagName('body')[0]; + if (!body) { + return false; + } + + return body.getAttribute('data-user-status') === 'logged-in'; +} diff --git a/app/assets/stylesheets/history.scss b/app/assets/stylesheets/history.scss new file mode 100644 index 000000000..32328c796 --- /dev/null +++ b/app/assets/stylesheets/history.scss @@ -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; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss index c8084dfe7..1bfa0c452 100644 --- a/app/assets/stylesheets/scaffolds.scss +++ b/app/assets/stylesheets/scaffolds.scss @@ -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; + } } diff --git a/app/controllers/history_controller.rb b/app/controllers/history_controller.rb new file mode 100644 index 000000000..b264c3c0d --- /dev/null +++ b/app/controllers/history_controller.rb @@ -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 diff --git a/app/controllers/reading_list_items_controller.rb b/app/controllers/reading_list_items_controller.rb index fbdd5916a..68053d5a4 100644 --- a/app/controllers/reading_list_items_controller.rb +++ b/app/controllers/reading_list_items_controller.rb @@ -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( diff --git a/app/javascript/history/history.jsx b/app/javascript/history/history.jsx new file mode 100644 index 000000000..ae76a84b1 --- /dev/null +++ b/app/javascript/history/history.jsx @@ -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 ( +