diff --git a/Envfile b/Envfile
index fdde4b2fc..cac827daa 100644
--- a/Envfile
+++ b/Envfile
@@ -103,9 +103,6 @@ variable :GA_OPTIMIZE_ID, :String, default: "Optional"
variable :GA_VIEW_ID, :String, default: "Optional"
variable :GA_FETCH_RATE, :Integer, default: 25
-# Honeycomb
-variable :HONEYCOMB_API_KEY, :String, default: "Optional"
-
# Mailchimp for mails
variable :MAILCHIMP_API_KEY, :String, default: "Optional-valid"
variable :MAILCHIMP_NEWSLETTER_ID, :String, default: "Optional"
diff --git a/Gemfile b/Gemfile
index 783a8d6cb..9a70330f0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,8 +12,10 @@ group :production do
gem 'nakayoshi_fork', '~> 0.0.4' # solves CoW friendly problem on MRI 2.2 and later
end
+
gem "actionpack-action_caching", "~> 1.2" # Action caching for Action Pack (removed from core in Rails 4.0)
gem "active_record_union", "~> 1.3" # Adds proper union and union_all methods to ActiveRecord::Relation
+gem "activerecord-import", "~> 1.0" # Adds ability to bulk create activerecord objects
gem "acts-as-taggable-on", "~> 6.0" # A tagging plugin for Rails applications that allows for custom tagging along dynamic contexts
gem "acts_as_follower", github: "thepracticaldev/acts_as_follower", branch: "master" # Allow any model to follow any other model
gem "addressable", "~> 2.5", ">= 2.5.2" # A replacement for the URI implementation that is part of Ruby's standard library
diff --git a/Gemfile.lock b/Gemfile.lock
index bbe017333..e78cdf54e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -59,6 +59,8 @@ GEM
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
+ activerecord-import (1.0.1)
+ activerecord (>= 3.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
@@ -960,6 +962,7 @@ PLATFORMS
DEPENDENCIES
actionpack-action_caching (~> 1.2)
active_record_union (~> 1.3)
+ activerecord-import (~> 1.0)
acts-as-taggable-on (~> 6.0)
acts_as_follower!
addressable (~> 2.5, >= 2.5.2)
diff --git a/app/assets/javascripts/initializers/initializeReadingListIcons.js b/app/assets/javascripts/initializers/initializeReadingListIcons.js
index 65f25d3ee..02cc21ab5 100644
--- a/app/assets/javascripts/initializers/initializeReadingListIcons.js
+++ b/app/assets/javascripts/initializers/initializeReadingListIcons.js
@@ -33,7 +33,7 @@ function addReadingListCountToHomePage() {
var readingListCount;
if (user && document.getElementById('reading-list-count')) {
readingListCount =
- user.reading_list_ids.length > 0 ? user.reading_list_ids.length : 'EMPTY';
+ user.reading_list_ids.length > 0 ? user.reading_list_ids.length : 'empty';
document.getElementById('reading-list-count').innerHTML =
'(' + readingListCount + ')';
document.getElementById('reading-list-count').dataset.count =
diff --git a/app/assets/javascripts/lib/pulltorefresh.js b/app/assets/javascripts/lib/pulltorefresh.js
index f17d2a419..251a05488 100644
--- a/app/assets/javascripts/lib/pulltorefresh.js
+++ b/app/assets/javascripts/lib/pulltorefresh.js
@@ -32,7 +32,7 @@
resistanceFunction: function (t) { return Math.min(1, t / 2.5); },
shouldPullToRefresh: function () {
return !window.scrollY &&
- (document.getElementById('articles-list') || document.getElementById("user-dashboard") || document.getElementById("article-body")) &&
+ (document.getElementById('articles-list') || document.getElementById("user-dashboard") || document.getElementById("article-body") || document.getElementById('classifieds-index-container')) &&
!document.body.classList.contains('modal-open')
},
};
diff --git a/app/assets/javascripts/serviceworker.js.erb b/app/assets/javascripts/serviceworker.js.erb
index c37aa0a36..fe9568a04 100644
--- a/app/assets/javascripts/serviceworker.js.erb
+++ b/app/assets/javascripts/serviceworker.js.erb
@@ -1,4 +1,4 @@
-var CACHE_VERSION = 'v2.6.4';
+var CACHE_VERSION = 'v2.6.5';
var CACHE_NAME = CACHE_VERSION + ':sw-cache::';
var REQUESTS_LIMIT = 70;
diff --git a/app/assets/stylesheets/articles.scss b/app/assets/stylesheets/articles.scss
index dac0b39c6..b7c0622f8 100644
--- a/app/assets/stylesheets/articles.scss
+++ b/app/assets/stylesheets/articles.scss
@@ -904,10 +904,19 @@
margin-bottom: 20px;
.sidebar-nav-block {
padding: 0px;
- width: calc(100% - 4px);
+ width: 100%;
+ box-sizing: border-box;
+ &.principle-sidebar-nav-block {
+ padding: 9px 0px;
+ .sidebar-nav-element {
+ .sidebar-nav-link {
+ padding: 5px 8px;
+ }
+ }
+ }
@media screen and (min-width: 950px) {
.sidebar-nav-block-inner {
- height: 400px;
+ height: 420px;
overflow-y: scroll;
overflow-x: hidden;
}
@@ -917,7 +926,10 @@
font-size: 0.9em;
font-family: $monospace;
&.sidebar-nav-header-middle {
- padding-top: 10px;
+ padding: 10px 10px;
+ text-decoration: underline;
+ color: $medium-gray;
+ color: var(--theme-secondary-color, $medium-gray);
}
img {
width: 15px;
@@ -946,48 +958,6 @@
display: block;
// font-family: "Lucida Console", Monaco, monospace, sans-serif;
position: relative;
- &.sidebar-nav-readinglist {
- font-family: $helvetica-condensed;
- font-stretch: condensed;
- font-size: 1.1em;
- .sidebar-nav-link {
- padding-top: 14px;
- padding-bottom: 14px;
- padding-left: 18px;
- display: block;
- &:hover {
- background: $light-purple;
- background: var(--theme-container-background, $light-purple);
- }
- }
- &:hover {
- background: white;
- background: var(--theme-background, white);
- }
- .readinglist-sidebar-subcat {
- font-weight: bold;
- font-size: 0.78em;
- font-family: $helvetica;
- padding-bottom: 10px;
- padding-left: 8px;
-
- a {
- color: $medium-gray;
- color: var(--theme-secondary-color, $medium-gray);
- padding: 7px 12px;
- display: inline-block;
- &:hover {
- background: darken($light-green, 5%);
- color: $dark-gray;
- }
- }
- .separator {
- border-left: 2px solid $light-medium-gray;
- padding: 4px 0px;
- margin: 0px 4px;
- }
- }
- }
@media screen and (min-width: 950px) {
.sidebar-nav-link-follow {
display: none;
@@ -1012,9 +982,10 @@
display: inline-block;
width: calc(100% - 10px);
font-weight: 500;
- .sidebar-nav-tag-text {
- border-radius: 3px;
- padding: 3px 8px;
+ padding: 3px 8px;
+ &:hover {
+ background: $light-gray;
+ background: var(--theme-container-background-hover, $light-gray);
}
}
a.sidebar-nav-link-follow {
@@ -1028,10 +999,6 @@
font-size: 0.8em;
padding: 2px 8px;
}
- &:hover {
- background: $light-gray;
- background: var(--theme-container-background-hover, $light-gray);
- }
}
}
}
@@ -1285,6 +1252,14 @@
display: inline-block;
text-align: center;
}
+ .widget-listing-category {
+ color: $medium-gray;
+ }
+ .widget-listing-title {
+ font-family: $helvetica;
+ font-weight: 500;
+ margin-bottom: 8px;
+ }
}
.query-filter-button {
font-size: 15px;
diff --git a/app/assets/stylesheets/classified_listings.scss b/app/assets/stylesheets/classified_listings.scss
new file mode 100644
index 000000000..d1918d4c3
--- /dev/null
+++ b/app/assets/stylesheets/classified_listings.scss
@@ -0,0 +1,255 @@
+@import 'variables';
+
+.classifieds-container {
+ margin: 60px auto 150px;
+ text-align: left;
+ max-width: 96%;
+ min-height: 100vh;
+ @media screen and (min-width: 950px) {
+ margin: 40px auto;
+ }
+ .classified-filters {
+ padding: 20px 0px 10px;
+ a {
+ padding: 3px 16px;
+ border-radius: 100px;
+ display: inline-block;
+ font-size: 15px;
+ &.selected {
+ background: $bold-blue;
+ color: white;
+ }
+ &.new-listing-button {
+ color: $black;
+ background: $green;
+ margin: 0px 4px;
+ }
+ }
+ .classified-filters-categories {
+ border: 1px solid $light-medium-gray;
+ padding: 10px;
+ border-radius: 8px;
+ a {
+ display: block;
+ &.classified-create-link {
+ border: 2px solid darken($green, 14%);
+ color: darken($green, 19%);
+ padding: 2px 16px;
+ }
+ }
+ @media screen and (min-width: 550px) {
+ a {
+ display: inline-block;
+ margin: 6px 3px;
+ border: 1px solid $bold-blue;
+ min-width: 50px;
+ text-align: center;
+ }
+ }
+ }
+ .classified-filters-tags {
+ padding: 20px 0px;
+ position: relative;
+ input {
+ font-size: 0.8em;
+ color: $black;
+ color: var(--theme-color, $black);
+ background: $light-gray;
+ background: var(--theme-container-accent-background, $light-gray);
+ border-radius: 100px;
+ padding: 3px 14px;
+ border: 1px solid $light-medium-gray;
+ width: calc(100% - 26px);
+ margin-bottom: 4px;
+ @media screen and (min-width: 550px) {
+ width: 280px;
+ margin-right: 8px;
+ }
+ }
+ .classified-search-clear {
+ background: $light-gray;
+ border: 1px solid $light-medium-gray;
+ position: absolute;
+ left: calc(100% - 25px);
+ top: 24px;
+ border-radius: 8px;
+ @media screen and (min-width: 550px) {
+ left: 282px;
+ }
+ }
+ .classified-tag {
+ font-size: 0.7em;
+ color: $black;
+ border-radius: 100px;
+ display: inline-block;
+ background: $dark-gray;
+ margin-right: 5px;
+ a {
+ padding: 0px 8px;
+ }
+ &.clear-all {
+ background: $light-medium-gray;
+ a { color: $black }
+ }
+ span {
+ color: white;
+ padding: 3px 2px;
+ display: inline-block;
+ &.tag-name {
+ padding-right: 0px;
+ }
+ }
+ }
+ }
+ }
+ .classifieds-columns {
+ display: grid;
+ grid-gap: 1em;
+ grid-template-columns: repeat(auto-fill, minmax(300px,1fr));
+ grid-auto-rows: 0;
+ .single-classified-listing {
+ width: 100%;
+ display: inline-block;
+ border: 1px solid $light-medium-gray;
+ margin: 0px auto;
+ box-sizing: border-box;
+ border-radius: 3px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 0.9em;
+ column-break-inside: avoid;
+ background: white;
+ background: var(--theme-container-background, white);
+ h3 {
+ margin: 0px;
+ padding: 9px 15px;
+ background: lighten($light-medium-gray, 8%);
+ background: var(--theme-container-accent-background, lighten($light-medium-gray, 8%));
+ border-bottom: 1px solid $light-medium-gray;
+ font-size: 16px;
+ }
+ .single-classified-listing-body {
+ padding: 10px 15px;
+ font-size: 15px;
+ .classified-listing-contact-cta {
+ border: 2px solid $black;
+ color: $black;
+ padding: 8px 10px;
+ text-align: center;
+ display: block;
+ border-radius: 200px;
+ margin: 4px auto;
+ width: 80%;
+ }
+ }
+ .single-classified-listing-tags {
+ padding: 10px 15px;
+ a {
+ color: $dark-medium-gray;
+ color: var(--theme-secondary-color, $dark-medium-gray);
+ background: $light-medium-gray;
+ background: var(--theme-container-accent-background, $light-medium-gray);
+ display: inline-block;
+ margin: 2px;
+ padding: 2px 7px;
+ font-size: 0.77em;
+ border-radius: 3px;
+ font-weight: bold;
+ }
+ }
+ .single-classified-listing-author-info {
+ font-size: 0.69em;
+ color: $medium-gray;
+ color: var(--theme-secondary-color, $medium-gray);
+ padding: 0px 15px 4px;
+ a {
+ color: $medium-gray;
+ color: var(--theme-secondary-color, $medium-gray);
+ font-style: italic;
+ }
+ }
+ }
+ }
+ form {
+ margin: 100px auto 20px;
+ width: 90%;
+ max-width: 600px;
+ * {
+ box-sizing: border-box;
+ }
+ .listings-back-buton {
+ font-size: 0.8em;
+ margin: 5px 0px 10px;
+ display: block;
+ }
+ .classified-errors {
+ background: $red;
+ color: white;
+ padding: 20px;
+ border-radius: 3px;
+ }
+ label {
+ display: inline-block;
+ font-size: 0.7em;
+ margin: 12px 0px 3px;
+ border-radius: 3px;
+ padding: 6px 0px;
+ font-weight: bold;
+ }
+ input {
+ width: 100%;
+ font-size: 18px;
+ padding: 5px;
+ border-radius: 3px;
+ border: 1px solid $light-medium-gray;
+ background: white;
+ background: var(--theme-container-background, white);
+ color: $black;
+ color: var(--theme-color, $black);
+ &[type="checkbox"] {
+ width: 100px;
+ }
+ }
+ textarea {
+ width: 100%;
+ height: 300px;
+ padding: 5px;
+ font-size: 17px;
+ border-radius: 3px;
+ background: white;
+ background: var(--theme-container-background, white);
+ color: $black;
+ color: var(--theme-color, $black);
+ border: 1px solid $light-medium-gray;
+ }
+ select {
+ width: 100%;
+ font-size:20px;
+ }
+ details {
+ cursor: pointer;
+ font-size: 0.8em;
+ padding: 8px 0px;
+ color: $medium-gray;
+ color: var(--theme-secondary-color, $medium-gray);
+ }
+ input[type="submit"] {
+ border: 0px;
+ padding: 25px 0px;
+ border-radius: 3px;
+ background: $blue;
+ font-size: 1.5em;
+ color: white;
+ margin-top: 20px;
+ }
+ .listings-current-credits {
+ margin-top: 75px;
+ border-radius: 8px;
+ font-weight: bold;
+ }
+
+ p {
+ font-size: 0.8em;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/credits.scss b/app/assets/stylesheets/credits.scss
new file mode 100644
index 000000000..8eae46df6
--- /dev/null
+++ b/app/assets/stylesheets/credits.scss
@@ -0,0 +1,192 @@
+@import 'variables';
+.credits-container {
+ margin: 50px auto;
+ .notice {
+ background: $green;
+ padding: 30px 0px;
+ margin-top: 68px;
+ text-align: center;
+ }
+ .credits-banner {
+ background: $dark-purple;
+ padding: 60px;
+ margin-top: 60px;
+ a {
+ color: white;
+ border: 3px solid white;
+ padding: 15px 4px;
+ border-radius: 100px;
+ font-weight: bold;
+ display: block;
+ margin: 10px auto;
+ text-align: center;
+ font-size: 14px;
+ width: 320px;
+ }
+ }
+
+ form#new_credit {
+ width: 500px;
+ max-width: 96%;
+ margin: 40px auto;
+ overflow: hidden;
+ }
+
+ .InputElement {
+ background-color: white;
+ background-color: var(--theme-container-background, white);
+ color: $black;
+ color: var(--theme-color, $black);
+ }
+
+ .StripeElement, input[type="number"], button, .existing-card {
+ box-sizing: border-box;
+
+ height: 40px;
+
+ padding: 10px 12px;
+
+ border: 1px solid transparent;
+ border-radius: 4px;
+ background-color: white;
+ background-color: var(--theme-container-background, white);
+ color: $black;
+ color: var(--theme-color, $black);
+
+ box-shadow: 0 1px 3px 0 #e6ebf1;
+ -webkit-transition: box-shadow 150ms ease;
+ transition: box-shadow 150ms ease;
+ margin-bottom: 20px;
+ }
+
+ .StripeElement--focus {
+ box-shadow: 0 1px 3px 0 #cfd7df;
+ }
+
+ .StripeElement--invalid {
+ border-color: #fa755a;
+ }
+
+ .StripeElement--webkit-autofill {
+ background-color: #fefde5 !important;
+ }
+
+ h1 {
+ text-align: center;
+ margin: 20px auto;
+ font-size: 2.5em;
+ border-radius: 3px;
+ padding: 30px 0px;
+ color: white;
+ }
+
+ .credit-price {
+ box-shadow: 0 1px 3px 0 #e6ebf1;
+ border-radius: 3px;
+ width: 80%;
+ margin: 10px auto;
+ padding: 30px 0px;
+ border: 1px solid $light-gray;
+ font-weight: bold;
+ display: inline-block;
+ font-size: 15px;
+ cursor: pointer;
+ .credit-amount {
+ font-size: 33px;
+ color: $dark-purple;
+ margin-bottom: 15px;
+ }
+ @media screen and ( min-width:950px ){
+ width: 290px;
+ }
+ }
+
+ input[type="number"] {
+ width: calc(100% - 160px);
+ font-size: 18px;
+ display: inline-block;
+ }
+
+ #calculated-price {
+ width: 140px;
+ padding-left: 10px;
+ color: $green;
+ font-size: 13px;
+ display: inline-block;
+ }
+
+ input[type="radio"] {
+ vertical-align: 2px;
+ padding: 10px;
+ cursor: pointer;
+ }
+
+ button {
+ width: 100%;
+ background: blue;
+ color: white;
+ font-size: 18px;
+ font-weight: bold;
+ }
+ button#add-new-card {
+ background-color: $light-gray;
+ background-color: var(--theme-container-background, $light-gray);
+ color: $black;
+ color: var(--theme-color, $black);
+ font-size: 15px;
+ }
+
+ .existing-card {
+ margin-bottom: 5px;
+ font-size: 0.7em;
+ padding: 0px;
+ padding-left: 10px;
+ label {
+ cursor: pointer;
+ display: inline-block;
+ width: calc(100% - 50px);
+ padding: 10px 0px;
+ }
+ .credit-card-component {
+ background: $lightest-gray;
+ background: var(--theme-container-accent-background, $lightest-gray);
+ border-radius: 3px;
+ margin: 0px 5px;
+ display: inline-block;
+ font-weight: bold;
+ }
+ }
+
+ label.main-form-label {
+ margin-bottom: 4px;
+ background: $light-gray;
+ background: var(--theme-container-accent-background, $light-gray);
+ border-radius: 3px;
+ padding: 2px 8px;
+ display: inline-block;
+ font-size: 0.7em;
+ font-weight: bold;
+ }
+
+ .existing-credits {
+ background: $dark-purple;
+ color: white;
+ width: 600px;
+ margin: 100px auto;
+ padding: 30px;
+ border-radius: 3px;
+ text-align: center;
+ .existing-credits-status {
+ font-size: 1.8em;
+ font-weight: bold;
+ padding: 30px 0px 50px;
+ }
+ a {
+ color: white;
+ border: 3px solid white;
+ padding: 10px 30px;
+ border-radius: 100px;
+ font-weight: bold;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/fundamentals.scss b/app/assets/stylesheets/fundamentals.scss
index 3b109b6ac..8b893e407 100644
--- a/app/assets/stylesheets/fundamentals.scss
+++ b/app/assets/stylesheets/fundamentals.scss
@@ -1,7 +1,3 @@
-#notice {
- color: green;
-}
-
#error_explanation {
width: 100%;
diff --git a/app/assets/stylesheets/giveaways.scss b/app/assets/stylesheets/giveaways.scss
deleted file mode 100644
index 0db578378..000000000
--- a/app/assets/stylesheets/giveaways.scss
+++ /dev/null
@@ -1,130 +0,0 @@
-@import 'variables';
-#page-content.giveaways{
- #error_explanation{
- margin-bottom:50px;
- }
-}
-.giveaway-header{
- font-family: $helvetica;
- height: calc(22vw + 250px);
- z-index:1;
- position:relative;
- margin-top:-50px;
- text-align:center;
- background: #000000 no-repeat center 0 fixed;
- background-size:110%;
- .logo{
- color:white;
- font-weight:900;
- padding-top: calc(7.5vw + 90px);
- margin:0;
- width: calc(18vw + 50px);
- }
- @media screen and ( min-width: 500px ){
- background-size:90%;
- }
- @media screen and ( min-width: 800px ){
- background-size:70%;
- }
- @media screen and ( min-width: 1100px ){
- background-size:60%;
- }
- @media screen and ( min-width: 1400px ){
- background-size:52%;
- }
-}
-
-.giveaway-container{
- font-family: $helvetica;
- background:white;
- padding:1vw 4vw;
- width:760px;
- max-width:calc(100% - 8vw);
- margin:0 auto 100px;
- z-index:2;
- position:relative;
- font-size:calc(0.4vw + 15px);
- line-height:calc(0.7vw + 26px);
- h1{
- text-align:center;
- font-size:calc(3vw + 20px);
- }
- p{
- font-size:1.1em;
- }
- img{
- max-width:100%;
- }
- h2{
- margin:2vw 0px;
- padding:0;
- }
-
- .sign-up-link{
- font-size:calc(1.2vw + 18px);
- margin:5px 0px;
- display:inline-block;
- .sign-up-text{
- background: $blue;
- padding:14px 20px;
- border-radius:5px;
- color:white;
- &.github-text{
- background: $black;
- }
- }
- img{
- width:calc(1.88vw + 22px);
- vertical-align:calc(-0.5vw - 4px);
- }
- &:hover{
- opacity:0.9;
- }
- }
- .partner-logo{
- width:200px;
- vertical-align:middle;
- margin:20px;
- }
- form{
-
- .field{
- label{
- padding:10px 0px;
- display:inline-block;
- font-size:0.95em;
- }
- input{
- width:calc(100% - 1vw - 6px);
- padding:calc(0.3vw + 4px);
- border:1px solid darken($light-medium-gray,5%);
- font-size:1em;
- border-radius:3px;
- }
- select{
- font-size:1em;
- }
- }
- .actions{
- padding:1vw 0px;
- input{
- background:$green;
- color:white;
- border:0px;
- font-size:2em;
- padding:10px 40px;
- border-radius:5px;
- cursor:pointer;
- &:hover{
- opacity:0.9;
- }
- }
- }
- }
-}
-
-.smarty-suggestion{
- font-size:0.88em;
- padding:0.15em 0.2em;
- font-family: $helvetica;
-}
diff --git a/app/assets/stylesheets/minimal.scss b/app/assets/stylesheets/minimal.scss
index c2137dd22..d89eb3a04 100644
--- a/app/assets/stylesheets/minimal.scss
+++ b/app/assets/stylesheets/minimal.scss
@@ -30,6 +30,8 @@
@import 'sticky-nav';
@import 'sidebar-data';
@import 'video-collection';
+@import 'classified_listings';
+@import 'credits';
@import 'ltags/LiquidTags';
diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss
index 608dc58d7..aee72676e 100644
--- a/app/assets/stylesheets/variables.scss
+++ b/app/assets/stylesheets/variables.scss
@@ -30,7 +30,7 @@ $outline-color: darken($light-medium-gray, 2%);
$shadow: 1px 2px 4px 0 rgba(0, 0, 0, 0.18);
$light-shadow: 1px 2px 4px 0 rgba(0, 0, 0, 0.1);
$dark-shadow: 1px 2px 4px 0 rgba(0, 0, 0, 0.25);
-$bold-shadow: 3px 3px 0px darken($light-medium-gray, 13%);
+$bold-shadow: 2px 2px 0px darken($light-medium-gray, 10%);
$ease-in-cubic : cubic-bezier(0.55, 0.055, 0.675, 0.19);
$ease-out-cubic : cubic-bezier(0.215, 0.61, 0.355, 1);
diff --git a/app/assets/stylesheets/video-collection.scss b/app/assets/stylesheets/video-collection.scss
index bf90ac3e0..d4ddb1102 100644
--- a/app/assets/stylesheets/video-collection.scss
+++ b/app/assets/stylesheets/video-collection.scss
@@ -3,15 +3,15 @@
.home.video-page-title {
margin: 68px auto 10px;
text-align: center;
- background: $white;
- background: var(--theme-container-background, #fff);
+ background: $lightest-gray;
+ background: var(--theme-container-background, $lightest-gray);
header {
font-size:calc(0.9vw + 10px);
}
}
.video-collection {
- background: $white;
- background: var(--theme-container-background, #fff);
+ background: $lightest-gray;
+ background: var(--theme-container-background, $lightest-gray);
display: flex;
max-width: 100%;
flex-direction: row;
@@ -23,15 +23,28 @@
}
.single-video-article {
- border: solid 1px $medium-gray;
+ border: solid 1px $light-medium-gray;
margin: 5px;
- max-width: 350px;
+ width: 100%;
padding-bottom: 8px;
- @media screen and (max-width: 739px) {
- min-width: 100%;
+ border-radius: 3px;
+ @media screen and (min-width: 550px) {
+ width: 47%;
+ }
+ @media screen and (min-width: 739px) {
+ width: 31%;
}
.video-image {
position: relative;
+ padding-top: 56%;
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ background: $black no-repeat center center;
+ -webkit-background-size: cover;
+ -moz-background-size: cover;
+ -o-background-size: cover;
+ background-size: cover;
+
}
.video-timestamp {
position: absolute;
diff --git a/app/controllers/classified_listings_controller.rb b/app/controllers/classified_listings_controller.rb
new file mode 100644
index 000000000..421213172
--- /dev/null
+++ b/app/controllers/classified_listings_controller.rb
@@ -0,0 +1,81 @@
+class ClassifiedListingsController < ApplicationController
+ before_action :set_classified_listing, only: [:show, :edit, :update]
+ before_action :set_cache_control_headers, only: %i[index]
+ after_action :verify_authorized, only: %i[edit update]
+ before_action :authenticate_user!, only: %i[edit update new]
+
+ def index
+ if params[:category].blank?
+ @classified_listings = ClassifiedListing.where(published: true).order("bumped_at DESC").limit(12)
+ else
+ @classified_listings = []
+ end
+ set_surrogate_key_header "classified-listings-#{params[:category]}"
+ end
+
+ def new
+ @classified_listing = ClassifiedListing.new
+ @credits = current_user.credits.where(spent: false)
+ end
+
+ def edit
+ authorize @classified_listing
+ @credits = current_user.credits.where(spent: false)
+ end
+
+ def create
+ @classified_listing = ClassifiedListing.new(classified_listing_params)
+ @classified_listing.user_id = current_user.id
+ number_of_credits_needed = ClassifiedListing.cost_by_category(@classified_listing.category)
+ available_credits = current_user.credits.where(spent: false)
+ if available_credits.size >= number_of_credits_needed
+ @classified_listing.bumped_at = Time.current
+ @classified_listing.published = true
+ @classified_listing.organization_id = current_user.organization_id if @classified_listing.post_as_organization.to_i == 1
+ if @classified_listing.save
+ CacheBuster.new.bust("/listings")
+ CacheBuster.new.bust("/listings?i=i")
+ available_credits.limit(number_of_credits_needed).update_all(spent: true)
+ redirect_to "/listings"
+ else
+ @credits = current_user.credits.where(spent: false)
+ render :new
+ end
+ else
+ redirect_to "/credits"
+ end
+ end
+
+ def update
+ authorize @classified_listing
+ available_credits = current_user.credits.where(spent: false)
+ number_of_credits_needed = ClassifiedListing.cost_by_category(@classified_listing.category) # Bumping
+ if params[:classified_listing][:action] == "bump"
+ @classified_listing.bumped_at = Time.current
+ if available_credits.size >= number_of_credits_needed
+ @classified_listing.save
+ available_credits.limit(number_of_credits_needed).update_all(spent: true)
+ end
+ elsif params[:classified_listing][:action] == "unpublish"
+ @classified_listing.published = false
+ @classified_listing.save
+ @classified_listing.remove_from_index!
+ elsif params[:classified_listing][:body_markdown].present? && @classified_listing.bumped_at > 24.hours.ago
+ @classified_listing.body_markdown = params[:classified_listing][:body_markdown]
+ @classified_listing.save
+ end
+ redirect_to "/listings"
+ end
+
+ private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_classified_listing
+ @classified_listing = ClassifiedListing.find(params[:id])
+ end
+
+ # Never trust parameters from the scary internet, only allow the white list through.
+ def classified_listing_params
+ accessible = %i[title body_markdown category tag_list contact_via_connect post_as_organization action]
+ params.require(:classified_listing).permit(accessible)
+ end
+end
diff --git a/app/controllers/credits_controller.rb b/app/controllers/credits_controller.rb
new file mode 100644
index 000000000..d3186e16b
--- /dev/null
+++ b/app/controllers/credits_controller.rb
@@ -0,0 +1,92 @@
+class CreditsController < ApplicationController
+ before_action :authenticate_user!
+
+ def index
+ @credits = current_user.credits.where(spent: false)
+ @org_credits = current_user.organization.credits.where(spent: false) if current_user.org_admin
+ end
+
+ def new
+ @credit = Credit.new
+ @purchaser = params[:purchaser] == "organization" ? current_user.organization : current_user
+ @customer = Stripe::Customer.retrieve(current_user.stripe_id_code) if current_user.stripe_id_code
+ end
+
+ def create
+ @number_to_purchase = params[:credit][:number_to_purchase].to_i
+ make_payment
+ credit_objects = []
+ @number_to_purchase.times do
+ if params[:user_type] == "organization"
+ raise unless current_user.org_admin
+ credit_objects << Credit.new(organization_id: current_user.organization_id, cost: cost_per_credit / 100.0)
+ else
+ credit_objects << Credit.new(user_id: current_user.id, cost: cost_per_credit / 100.0)
+ end
+ end
+ Credit.import credit_objects
+ redirect_to "/credits", notice: "#{@number_to_purchase} new credits purchased!"
+ end
+
+ def make_payment
+ find_or_create_customer
+ find_or_create_card
+ update_user_stripe_info
+ create_charge
+ rescue Stripe::CardError => e
+ flash[:error] = e.message
+ redirect_to "/credits/new"
+ end
+
+ def find_or_create_customer
+ @customer = if current_user.stripe_id_code
+ Stripe::Customer.retrieve(current_user.stripe_id_code)
+ else
+ Stripe::Customer.create({
+ email: current_user.email
+ })
+ end
+ end
+
+ def find_or_create_card
+ if params[:stripe_token]
+ @card = Stripe::Customer.create_source(
+ @customer.id,
+ {
+ source: params[:stripe_token],
+ }
+ )
+ else
+ @card = @customer.sources.retrieve(params[:selected_card])
+ end
+ end
+
+ def update_user_stripe_info
+ current_user.update_column(:stripe_id_code, @customer.id) if current_user.stripe_id_code.nil?
+ end
+
+ def create_charge
+ @amount = generate_cost
+ Stripe::Charge.create({
+ customer: @customer.id,
+ source: @card || @customer.default_source,
+ amount: @amount,
+ description: "Purchase of #{@number_to_purchase} credits.",
+ currency: "usd"
+ })
+ end
+
+ def generate_cost
+ @number_to_purchase * cost_per_credit
+ end
+
+ def cost_per_credit
+ if @number_to_purchase < 50
+ 1000
+ elsif @number_to_purchase < 500
+ 750
+ else
+ 500
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0f3471512..6ebc454da 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -30,6 +30,8 @@ module ApplicationHelper
notifications
reading_list_items
html_variants
+ classified_listings
+ credits
].include?(controller_name)
end
diff --git a/app/javascript/listings/listings.jsx b/app/javascript/listings/listings.jsx
new file mode 100644
index 000000000..3347481ee
--- /dev/null
+++ b/app/javascript/listings/listings.jsx
@@ -0,0 +1,233 @@
+import { h, Component } from 'preact';
+import { SingleListing } from './singleListing';
+
+export class Listings extends Component {
+ state = {
+ listings: [],
+ query: '',
+ tags: [],
+ index: null,
+ category: '',
+ allCategories: [],
+ initialFetch: true,
+ currentUserId: null,
+ };
+
+ componentWillMount() {
+ const params = this.getQueryParams();
+ const t = this;
+ const algoliaId = document.querySelector("meta[name='algolia-public-id']")
+ .content;
+ const algoliaKey = document.querySelector("meta[name='algolia-public-key']")
+ .content;
+ const env = document.querySelector("meta[name='environment']").content;
+ const client = algoliasearch(algoliaId, algoliaKey);
+ const index = client.initIndex(`ClassifiedListing_${env}`);
+ const container = document.getElementById('classifieds-index-container')
+ const category = container.dataset.category || ''
+ const allCategories = JSON.parse(container.dataset.allcategories || []);
+ let tags = [];
+ if (params.t) {
+ tags = params.t.split(',')
+ }
+ const query = params.q || ''
+ let listings = [];
+ if (tags.length === 0 && query === '') {
+ listings = JSON.parse(container.dataset.listings)
+ }
+ t.setState({query, tags, index, category, allCategories, listings });
+ t.listingSearch(params.q || '', tags, category);
+ t.setUser()
+ }
+
+ addTag = (e, tag) => {
+ e.preventDefault();
+ const { query, tags, category } = this.state;
+ const newTags = tags;
+ if (newTags.indexOf(tag) === -1) {
+ newTags.push(tag)
+ }
+ this.setState({tags: newTags})
+ this.listingSearch(query, newTags, category)
+ window.scroll(0,0)
+ }
+
+ removeTag = (e, tag) => {
+ e.preventDefault();
+ const { query, tags, category } = this.state;
+ const newTags = tags;
+ const index = newTags.indexOf(tag);
+ if (newTags.indexOf(tag) > -1) {
+ newTags.splice(index, 1);
+ }
+ this.setState({tags: newTags})
+ this.listingSearch(query, newTags, category)
+ }
+
+ selectCategory = (e, cat) => {
+ e.preventDefault();
+ const { query, tags } = this.state;
+ this.setState({category: cat})
+ this.listingSearch(query, tags, cat)
+ }
+
+ handleQuery = e => {
+ const { tags, category } = this.state;
+ this.setState({query: e.target.value})
+ this.listingSearch(e.target.value, tags, category)
+ }
+
+ clearQuery = () => {
+ const { tags, category } = this.state;
+ document.getElementById('listings-search').value = '';
+ this.setState({query: ''});
+ this.listingSearch('', tags, category);
+ }
+
+ getQueryParams = () => {
+ let qs = document.location.search;
+ qs = qs.split('+').join(' ');
+
+ const params = {};
+ let tokens;
+ const re = /[?&]?([^=]+)=([^&]*)/g;
+
+ // eslint-disable-next-line no-cond-assign
+ while (tokens = re.exec(qs)) {
+ params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
+ }
+
+ return params;
+ }
+
+ setUser = () => {
+ const t = this;
+ setTimeout(function() {
+ if (window.currentUser && t.state.currentUserId === null) {
+ t.setState({currentUserId: window.currentUser.id });
+ }
+ }, 300)
+ setTimeout(function() {
+ if (window.currentUser && t.state.currentUserId === null) {
+ t.setState({currentUserId: window.currentUser.id });
+ }
+ }, 1000)
+ }
+
+ triggerMasonry = () => {
+ resizeAllMasonryItems();
+ setTimeout(function() {
+ resizeAllMasonryItems();
+ }, 1)
+ setTimeout(function() {
+ resizeAllMasonryItems();
+ }, 3)
+ }
+
+ listingSearch(query, tags, category) {
+ const t = this;
+ const filterObject = {tagFilters: tags, hitsPerPage: 120,}
+ if (category.length > 0) {
+ filterObject.filters = `category:${category}`
+ }
+ t.state.index.search(query, filterObject)
+ .then(function searchDone(content) {
+ if (t.state.initialFetch && t.state.category === '') {
+ const fullListings = t.state.listings;
+ content.hits.forEach(function(listing) {
+ if (!t.state.listings.map(l => (l.id)).includes(listing.id)) {
+ fullListings.push(listing)
+ }
+ });
+ t.setState({listings: fullListings, initialFetch: false});
+ } else {
+ t.setState({listings: content.hits, initialFetch: false});
+ }
+
+ });
+ let newLocation = ''
+ if (query.length > 0 && tags.length > 0) {
+ newLocation = `/listings/${category}?q=${query}&t=${tags}`;
+ } else if (query.length > 0){
+ newLocation = `/listings/${category}?q=${query}`;
+ } else if (tags.length > 0) {
+ newLocation = `/listings/${category}?t=${tags}`;
+ } else if (category.length > 0) {
+ newLocation = `/listings/${category}`;
+ } else {
+ newLocation = '/listings'
+ }
+ window.history.replaceState(null, null, newLocation);
+ }
+
+ render() {
+ const { listings, query, tags, category, allCategories, currentUserId } = this.state;
+ const allListings = listings.map(listing => (
+
*All org admins have access to credits owned by organization
+ <% end %> +Email yo@dev.to if you need support
+ <% end %> + + \ No newline at end of file diff --git a/app/views/layouts/_styles.html.erb b/app/views/layouts/_styles.html.erb index 8ca5f7a5b..f2f7bd93a 100644 --- a/app/views/layouts/_styles.html.erb +++ b/app/views/layouts/_styles.html.erb @@ -72,6 +72,20 @@ <%= Rails.application.assets["leaderboard.css"].to_s.html_safe %> <%= Rails.application.assets["footer.css"].to_s.html_safe %> + <% elsif view_class.include? "classified_listings-" %> + + <% elsif view_class.include? "credits-" %> + <% elsif view_class.include? "videos-index" %>