Listing Drafts and Deletion (#3540)
* wip init handle draft class and styling * add draft button * start splitting up listing row into multile files * separate tag links * split location * fix location * separate listing row action buttons * update listing row proptypes to describe listing object * contact via connect to jsx * fix default checked * init draft creation and first publish charge * fix first publish logic and credit charging * handle drafts in dashboard filtering * adjust isDraft bool statement * hide drafts from main listings feed * fix expired and draft bools in listing row * adjust create listing org credits * internally handle drafts * adjust listing row * break down update method * remove unnecessary logic and shorten lines * fix logic again woops * convert input hidden value + submit into one form button submit with corresponding name and value also adds clear message that drafts do not cost credits * handle insufficient org credits on first publish - uses user credits to publish - similar to bump implementation * fix sorting for personal and all orgs * space out some elements * don't spend original users credit for them * add delete buttons and delete_confirm and destroy methods * remove notification removal since listings dont trigger notifications * move draft message and add draft_params to tests * Update classified_listings_controller.rb * update tests * update snapshot * add del tests * add guard preventing free draft publish * fix draft filter * update from master and resolve merge conflict in listing preact * Revert "update from master and resolve merge conflict in listing preact" This reverts commit 0a34fccf334a2ca0902644b486cb26ead6bef664. * update column spec instead of setting and saving * separate two expectations into their own tests * split more tests into single expectations * change to unless bumped_at? instead of if nil * Fix listing draft edge cases and styling * Add title tags to listings pages
This commit is contained in:
parent
3cf6a2d4bc
commit
fda048dc12
29 changed files with 726 additions and 232 deletions
|
|
@ -6,6 +6,9 @@
|
|||
text-align: left;
|
||||
max-width: 100%;
|
||||
min-height: 100vh;
|
||||
&.classifieds-form-container {
|
||||
margin: 90px auto 150px;
|
||||
}
|
||||
@media screen and (min-width: 950px) {
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
|
@ -143,7 +146,7 @@
|
|||
);
|
||||
@media screen and (min-width: 580px) {
|
||||
border-radius: 8px;
|
||||
margin: 100px auto 20px;
|
||||
margin: 20px auto 20px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -268,6 +271,10 @@
|
|||
theme-container-box-shadow,
|
||||
2px 2px 8px darken($light-medium-gray, 5%)
|
||||
);
|
||||
&.cta-draft {
|
||||
color: $black;
|
||||
background: $yellow;
|
||||
}
|
||||
}
|
||||
.listings-current-credits {
|
||||
position: fixed;
|
||||
|
|
@ -611,7 +618,9 @@ form.listings-contact-via-connect {
|
|||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
user-select: none;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
|
|
@ -740,9 +749,15 @@ form.listings-contact-via-connect {
|
|||
border-radius: 3px;
|
||||
position: relative;
|
||||
background: var(--theme-container-background, #fff);
|
||||
|
||||
&.draft {
|
||||
background: var(--theme-container-accent-background, #fffeec)
|
||||
}
|
||||
|
||||
&.expired {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.listing-org {
|
||||
@include themeable(
|
||||
background,
|
||||
|
|
@ -824,6 +839,11 @@ form.listings-contact-via-connect {
|
|||
background: $green;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&.yellow {
|
||||
background: $yellow;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&.red {
|
||||
background: $red;
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: 426px) {
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@
|
|||
display: inline-block;
|
||||
padding: 14px;
|
||||
|
||||
@media screen and (max-width: 426px) {
|
||||
@media screen and (max-width: 600px) {
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
@import 'variables';
|
||||
|
||||
.delete-confirm{
|
||||
padding:150px 10px;
|
||||
min-height:300px;
|
||||
h4{
|
||||
background:$black;
|
||||
padding:5px 8px;
|
||||
color:white;
|
||||
display:inline-block;
|
||||
margin-bottom:0px;
|
||||
}
|
||||
h2{
|
||||
font-size:22px;
|
||||
margin-bottom:40px;
|
||||
}
|
||||
a.delete-link{
|
||||
background:$red;
|
||||
color:white;
|
||||
padding:10px 15px;
|
||||
&:hover{
|
||||
color:white;
|
||||
opacity:0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@
|
|||
@import 'signup-modal';
|
||||
@import 'tags';
|
||||
@import 'live';
|
||||
@import 'delete-confirm';
|
||||
@import 'preact/sidebar-widget';
|
||||
@import 'preact/article-form';
|
||||
@import 'tag-edit';
|
||||
|
|
|
|||
|
|
@ -221,3 +221,28 @@ body.comic-sans-article-body {
|
|||
body.pro-status-true .pro-visible-block {
|
||||
display: block !important;;
|
||||
}
|
||||
|
||||
.delete-confirm{
|
||||
padding:150px 10px;
|
||||
min-height:300px;
|
||||
h4{
|
||||
background:$black;
|
||||
padding:5px 8px;
|
||||
color:white;
|
||||
display:inline-block;
|
||||
margin-bottom:0px;
|
||||
}
|
||||
h2{
|
||||
font-size:22px;
|
||||
margin-bottom:40px;
|
||||
}
|
||||
a.delete-link{
|
||||
background:$red;
|
||||
color:white;
|
||||
padding:10px 15px;
|
||||
&:hover{
|
||||
color:white;
|
||||
opacity:0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
class ClassifiedListingsController < ApplicationController
|
||||
include ClassifiedListingsToolkit
|
||||
before_action :set_classified_listing, only: %i[edit update]
|
||||
before_action :set_classified_listing, only: %i[edit update destroy]
|
||||
before_action :set_cache_control_headers, only: %i[index]
|
||||
before_action :raise_banned, only: %i[new create update]
|
||||
after_action :verify_authorized, only: %i[edit update]
|
||||
|
|
@ -45,9 +45,21 @@ class ClassifiedListingsController < ApplicationController
|
|||
|
||||
@classified_listing.user_id = current_user.id
|
||||
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
|
||||
|
||||
org = Organization.find_by(id: @classified_listing.organization_id)
|
||||
|
||||
if listing_params[:action] == "draft"
|
||||
@classified_listing.published = false
|
||||
if @classified_listing.save
|
||||
redirect_to "/listings/dashboard"
|
||||
else
|
||||
@credits = current_user.credits.unspent
|
||||
@classified_listing.cached_tag_list = listing_params[:tag_list]
|
||||
@organizations = current_user.organizations
|
||||
render :new
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
available_org_credits = org.credits.unspent if org
|
||||
available_user_credits = current_user.credits.unspent
|
||||
|
||||
|
|
@ -64,27 +76,23 @@ class ClassifiedListingsController < ApplicationController
|
|||
def update
|
||||
authorize @classified_listing
|
||||
|
||||
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
|
||||
|
||||
# NOTE: this should probably be split in three different actions: bump, unpublish, publish
|
||||
if listing_params[:action] == "bump"
|
||||
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
|
||||
return bump_listing(cost) if listing_params[:action] == "bump"
|
||||
|
||||
org = Organization.find_by(id: @classified_listing.organization_id)
|
||||
|
||||
available_org_credits = org.credits.unspent if org
|
||||
available_user_credits = current_user.credits.unspent
|
||||
|
||||
if org && available_org_credits.size >= cost
|
||||
charge_credits_before_bump(org, cost)
|
||||
elsif available_user_credits.size >= cost
|
||||
charge_credits_before_bump(current_user, cost)
|
||||
else
|
||||
redirect_to(credits_path, notice: "Not enough available credits") && return
|
||||
end
|
||||
elsif listing_params[:action] == "unpublish"
|
||||
if listing_params[:action] == "unpublish"
|
||||
unpublish_listing
|
||||
redirect_to "/listings/dashboard"
|
||||
return
|
||||
elsif listing_params[:action] == "publish"
|
||||
unless @classified_listing.bumped_at?
|
||||
first_publish(cost)
|
||||
return
|
||||
end
|
||||
|
||||
publish_listing
|
||||
elsif listing_params[:body_markdown].present? && @classified_listing.bumped_at > 24.hours.ago
|
||||
elsif listing_params[:body_markdown].present? && ((@classified_listing.bumped_at && @classified_listing.bumped_at > 24.hours.ago) || !@classified_listing.published)
|
||||
update_listing_details
|
||||
end
|
||||
|
||||
|
|
@ -104,6 +112,17 @@ class ClassifiedListingsController < ApplicationController
|
|||
@user_credits = current_user.unspent_credits_count
|
||||
end
|
||||
|
||||
def delete_confirm
|
||||
@classified_listing = ClassifiedListing.find_by(slug: params[:slug])
|
||||
authorize @classified_listing
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @classified_listing
|
||||
@classified_listing.destroy!
|
||||
redirect_to "/listings/dashboard", notice: "Listing was successfully deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_listing(purchaser, cost)
|
||||
|
|
@ -140,6 +159,37 @@ class ClassifiedListingsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def first_publish(cost)
|
||||
available_author_credits = @classified_listing.author.credits.unspent
|
||||
available_user_credits = []
|
||||
if @classified_listing.author.is_a?(Organization)
|
||||
available_user_credits = current_user.credits.unspent
|
||||
end
|
||||
|
||||
if available_author_credits.size >= cost
|
||||
create_listing(@classified_listing.author, cost)
|
||||
elsif available_user_credits.size >= cost
|
||||
create_listing(current_user, cost)
|
||||
else
|
||||
redirect_to credits_path, notice: "Not enough available credits"
|
||||
end
|
||||
end
|
||||
|
||||
def bump_listing(cost)
|
||||
org = Organization.find_by(id: @classified_listing.organization_id)
|
||||
|
||||
available_org_credits = org.credits.unspent if org
|
||||
available_user_credits = current_user.credits.unspent
|
||||
|
||||
if org && available_org_credits.size >= cost
|
||||
charge_credits_before_bump(org, cost)
|
||||
elsif available_user_credits.size >= cost
|
||||
charge_credits_before_bump(current_user, cost)
|
||||
else
|
||||
redirect_to(credits_path, notice: "Not enough available credits") && return
|
||||
end
|
||||
end
|
||||
|
||||
def charge_credits_before_bump(purchaser, cost)
|
||||
ActiveRecord::Base.transaction do
|
||||
Credits::Buyer.call(
|
||||
|
|
@ -148,7 +198,7 @@ class ClassifiedListingsController < ApplicationController
|
|||
cost: cost,
|
||||
)
|
||||
|
||||
raise ActiveRecord::Rollback unless bump_listing
|
||||
raise ActiveRecord::Rollback unless bump_listing_success
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ module ClassifiedListingsToolkit
|
|||
@classified_listing.save
|
||||
end
|
||||
|
||||
def bump_listing
|
||||
def bump_listing_success
|
||||
@classified_listing.bumped_at = Time.current
|
||||
saved = @classified_listing.save
|
||||
@classified_listing.index! if saved
|
||||
|
|
|
|||
|
|
@ -63,6 +63,14 @@ preact-render-spy (1 nodes)
|
|||
>
|
||||
Active
|
||||
</span>
|
||||
<span
|
||||
onClick={[Function onClick]}
|
||||
class="rounded-btn "
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
Draft
|
||||
</span>
|
||||
<span
|
||||
onClick={[Function onClick]}
|
||||
class="rounded-btn "
|
||||
|
|
@ -72,7 +80,7 @@ preact-render-spy (1 nodes)
|
|||
Expired
|
||||
</span>
|
||||
</div>
|
||||
<select onChange={[Function sortListings]}>
|
||||
<select onChange={[Function onChange]}>
|
||||
<option
|
||||
value="created_at"
|
||||
selected="selected"
|
||||
|
|
@ -83,28 +91,47 @@ preact-render-spy (1 nodes)
|
|||
</select>
|
||||
</div>
|
||||
<div class="dashboard-listings-view">
|
||||
<div class="dashboard-listing-row expired">
|
||||
<span class="listing-org">Infotrode</span>
|
||||
<a href="cfp/hehhehe-5hld">
|
||||
<h2>hehhehe (expired)</h2>
|
||||
<div class="dashboard-listing-row expired">
|
||||
<a href="23/edit">
|
||||
<h2>asdfasdf (expired)</h2>
|
||||
</a>
|
||||
<span class="dashboard-listing-date">Jun 11</span>
|
||||
<span class="dashboard-listing-category">
|
||||
<a href="/listings/cfp/">cfp</a>
|
||||
</span>
|
||||
<span class="dashboard-listing-tags"></span>
|
||||
<span class="dashboard-listing-tags">
|
||||
<a
|
||||
href="/listings?t=computerscience"
|
||||
data-no-instant={true}
|
||||
>
|
||||
#computerscience
|
||||
</a>
|
||||
<a
|
||||
href="/listings?t=career"
|
||||
data-no-instant={true}
|
||||
>
|
||||
#career
|
||||
</a>
|
||||
</span>
|
||||
<div class="listing-row-actions">
|
||||
<a
|
||||
href="/listings/25/edit"
|
||||
href="/listings/23/edit"
|
||||
class="dashboard-listing-edit-button cta pill green"
|
||||
>
|
||||
EDIT
|
||||
</a>
|
||||
<a
|
||||
href="/listings/cfp/asdfasdf-2ea8/delete_confirm"
|
||||
class="dashboard-listing-delete-button cta pill black"
|
||||
data-no-instant={true}
|
||||
>
|
||||
DELETE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-listing-row expired">
|
||||
<div class="dashboard-listing-row expired">
|
||||
<span class="listing-org">Yoyodyne</span>
|
||||
<a href="events/yoyoyoyoyoooooooo-4jcb">
|
||||
<a href="24/edit">
|
||||
<h2>YOYOYOYOYOOOOOOOO (expired)</h2>
|
||||
</a>
|
||||
<span class="dashboard-listing-date">Jun 11</span>
|
||||
|
|
@ -138,40 +165,42 @@ preact-render-spy (1 nodes)
|
|||
>
|
||||
EDIT
|
||||
</a>
|
||||
<a
|
||||
href="/listings/events/yoyoyoyoyoooooooo-4jcb/delete_confirm"
|
||||
class="dashboard-listing-delete-button cta pill black"
|
||||
data-no-instant={true}
|
||||
>
|
||||
DELETE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-listing-row expired">
|
||||
<a href="cfp/asdfasdf-2ea8">
|
||||
<h2>asdfasdf (expired)</h2>
|
||||
<div class="dashboard-listing-row expired">
|
||||
<span class="listing-org">Infotrode</span>
|
||||
<a href="25/edit">
|
||||
<h2>hehhehe (expired)</h2>
|
||||
</a>
|
||||
<span class="dashboard-listing-date">Jun 11</span>
|
||||
<span class="dashboard-listing-category">
|
||||
<a href="/listings/cfp/">cfp</a>
|
||||
</span>
|
||||
<span class="dashboard-listing-tags">
|
||||
<a
|
||||
href="/listings?t=computerscience"
|
||||
data-no-instant={true}
|
||||
>
|
||||
#computerscience
|
||||
</a>
|
||||
<a
|
||||
href="/listings?t=career"
|
||||
data-no-instant={true}
|
||||
>
|
||||
#career
|
||||
</a>
|
||||
</span>
|
||||
<span class="dashboard-listing-tags"></span>
|
||||
<div class="listing-row-actions">
|
||||
<a
|
||||
href="/listings/23/edit"
|
||||
href="/listings/25/edit"
|
||||
class="dashboard-listing-edit-button cta pill green"
|
||||
>
|
||||
EDIT
|
||||
</a>
|
||||
<a
|
||||
href="/listings/cfp/hehhehe-5hld/delete_confirm"
|
||||
class="dashboard-listing-delete-button cta pill black"
|
||||
data-no-instant={true}
|
||||
>
|
||||
DELETE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,72 +1,77 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
import ListingDate from './rowElements/listingDate';
|
||||
import Tags from './rowElements/tags';
|
||||
import Location from './rowElements/location';
|
||||
import ActionButtons from './rowElements/actionButtons';
|
||||
|
||||
export const ListingRow = ({ listing }) => {
|
||||
const tagLinks = listing.tag_list.map(tag => (
|
||||
<a href={`/listings?t=${tag}`} data-no-instant>
|
||||
#{tag}{' '}
|
||||
</a>
|
||||
));
|
||||
const bumpedAt = listing.bumped_at ? listing.bumped_at.toString() : null;
|
||||
const isExpired =
|
||||
bumpedAt && !listing.published
|
||||
? (Date.now() - new Date(bumpedAt).getTime()) / (1000 * 60 * 60 * 24) > 30
|
||||
: false;
|
||||
const isDraft = bumpedAt ? !isExpired && !listing.published : true;
|
||||
const listingUrl = listing.published
|
||||
? `${listing.category}/${listing.slug}`
|
||||
: `${listing.id}/edit`;
|
||||
|
||||
const listingLocation = listing.location ? (` ・ ${listing.location}`) : '';
|
||||
|
||||
const listingDate = listing.bumped_at
|
||||
? new Date(listing.bumped_at.toString()).toLocaleDateString('default', {
|
||||
const expiryDate = listing.expires_at
|
||||
? new Date(listing.expires_at.toString()).toLocaleDateString('default', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
})
|
||||
: new Date(listing.updated_at.toString()).toLocaleDateString('default', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
});
|
||||
: '';
|
||||
|
||||
const orgName = listing.organization_id ? (
|
||||
<span className="listing-org">{listing.author.name}</span>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
const expiryDate = listing.expires_at ?
|
||||
new Date(listing.expires_at.toString()).toLocaleDateString('default', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
}) : '' ;
|
||||
|
||||
const listingExpiry = expiryDate !== '' ? (
|
||||
` | Expires on: ${expiryDate}`
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const listingExpiry = expiryDate !== '' ? ` | Expires on: ${expiryDate}` : '';
|
||||
|
||||
return (
|
||||
<div className={`dashboard-listing-row ${listing.published ? '' : 'expired'}`}>
|
||||
{orgName}
|
||||
<a href={`${listing.category}/${listing.slug}`}>
|
||||
<h2>{listing.title + (listing.published ? '' : " (expired)")}</h2>
|
||||
<div
|
||||
className={`dashboard-listing-row ${isDraft ? 'draft' : ''} ${
|
||||
isExpired ? 'expired' : ''
|
||||
}`}
|
||||
>
|
||||
{listing.organization_id && (
|
||||
<span className="listing-org">{listing.author.name}</span>
|
||||
)}
|
||||
<a href={listingUrl}>
|
||||
<h2>{listing.title + (isExpired ? ' (expired)' : '')}</h2>
|
||||
</a>
|
||||
<span className="dashboard-listing-date">
|
||||
{listingDate}
|
||||
{listingExpiry}
|
||||
{listingLocation}
|
||||
</span>
|
||||
<ListingDate
|
||||
bumpedAt={listing.bumped_at}
|
||||
updatedAt={listing.updated_at}
|
||||
/>
|
||||
{listingExpiry}
|
||||
{listing.location && <Location location={listing.location} />}
|
||||
<span className="dashboard-listing-category">
|
||||
<a href={`/listings/${listing.category}/`}>{listing.category}</a>
|
||||
</span>
|
||||
<span className="dashboard-listing-tags">{tagLinks}</span>
|
||||
<div className="listing-row-actions">
|
||||
{/* <a className="dashboard-listing-bump-button cta pill black">BUMP</a> */}
|
||||
<a
|
||||
href={`/listings/${listing.id}/edit`}
|
||||
className="dashboard-listing-edit-button cta pill green"
|
||||
>
|
||||
EDIT
|
||||
</a>
|
||||
{/* <a className="dashboard-listing-delete-button cta pill red">DELETE</a> */}
|
||||
</div>
|
||||
<Tags tagList={listing.tag_list} />
|
||||
<ActionButtons
|
||||
isDraft={isDraft}
|
||||
listingUrl={`${listing.category}/${listing.slug}`}
|
||||
editUrl={`/listings/${listing.id}/edit`}
|
||||
deleteConfirmUrl={`/listings/${listing.category}/${listing.slug}/delete_confirm`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListingRow.propTypes = {
|
||||
listing: PropTypes.object.isRequired,
|
||||
listing: PropTypes.PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
tag_list: PropTypes.arrayOf(PropTypes.string),
|
||||
created_at: PropTypes.instanceOf(Date),
|
||||
bumped_at: PropTypes.instanceOf(Date),
|
||||
updated_at: PropTypes.instanceOf(Date),
|
||||
category: PropTypes.string.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
organization_id: PropTypes.number,
|
||||
location: PropTypes.string,
|
||||
expires_at: PropTypes.bool,
|
||||
published: PropTypes.bool.isRequired,
|
||||
author: PropTypes.object,
|
||||
}).isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
const ActionButtons = ({ isDraft, editUrl, deleteConfirmUrl }) => {
|
||||
return (
|
||||
<div className="listing-row-actions">
|
||||
{/* <a className="dashboard-listing-bump-button cta pill black">BUMP</a> */}
|
||||
{isDraft && (
|
||||
<a
|
||||
href={editUrl}
|
||||
className="dashboard-listing-edit-button cta pill yellow"
|
||||
>
|
||||
DRAFT
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={editUrl}
|
||||
className="dashboard-listing-edit-button cta pill green"
|
||||
>
|
||||
EDIT
|
||||
</a>
|
||||
<a
|
||||
href={deleteConfirmUrl}
|
||||
className="dashboard-listing-delete-button cta pill black"
|
||||
data-no-instant
|
||||
>
|
||||
DELETE
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButtons.propTypes = {
|
||||
isDraft: PropTypes.bool.isRequired,
|
||||
editUrl: PropTypes.string.isRequired,
|
||||
deleteConfirmUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ActionButtons;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
const ListingDate = ({ bumpedAt, updatedAt }) => {
|
||||
|
||||
const listingDate = bumpedAt
|
||||
? new Date(bumpedAt.toString()).toLocaleDateString('default', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
})
|
||||
: new Date(updatedAt.toString()).toLocaleDateString('default', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
});
|
||||
|
||||
return (
|
||||
<span className="dashboard-listing-date">
|
||||
{listingDate}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
ListingDate.propTypes = {
|
||||
bumpedAt: PropTypes.instanceOf(Date).isRequired,
|
||||
updatedAt: PropTypes.instanceOf(Date).isRequired,
|
||||
}
|
||||
|
||||
export default ListingDate;
|
||||
17
app/javascript/listings/dashboard/rowElements/location.jsx
Normal file
17
app/javascript/listings/dashboard/rowElements/location.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
const Location = ({ location }) => {
|
||||
return (
|
||||
<span className="dashboard-listing-date">
|
||||
・
|
||||
{location}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
Location.propTypes = {
|
||||
location: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default Location;
|
||||
25
app/javascript/listings/dashboard/rowElements/tags.jsx
Normal file
25
app/javascript/listings/dashboard/rowElements/tags.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
const Tags = ({ tagList }) => {
|
||||
const tagLinks = tagList.map(tag => (
|
||||
<a href={`/listings?t=${tag}`} data-no-instant>
|
||||
#
|
||||
{tag}
|
||||
{' '}
|
||||
</a>
|
||||
));
|
||||
|
||||
return (
|
||||
<span className="dashboard-listing-tags">
|
||||
{tagLinks}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Tags.propTypes = {
|
||||
tagList: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
}
|
||||
|
||||
export default Tags;
|
||||
25
app/javascript/listings/elements/contactViaConnect.jsx
Normal file
25
app/javascript/listings/elements/contactViaConnect.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ContactViaConnect = ({ onChange, checked }) => (
|
||||
<div className="field">
|
||||
<label className="listingform__label" htmlFor="contact_via_connect">
|
||||
Allow Users to Message Me Via In-App Chat (DEV Connect)
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="listingform__input listingform__contact_via_connect"
|
||||
id="contact_via_connect"
|
||||
name="classified_listing[contact_via_connect]"
|
||||
onInput={onChange}
|
||||
checked
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
ContactViaConnect.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default ContactViaConnect;
|
||||
|
|
@ -9,6 +9,7 @@ export class ListingDashboard extends Component {
|
|||
userCredits: 0,
|
||||
selectedListings: 'user',
|
||||
filter: 'All',
|
||||
sort: 'created_at',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -32,24 +33,43 @@ export class ListingDashboard extends Component {
|
|||
orgs,
|
||||
selectedListings,
|
||||
filter,
|
||||
sort
|
||||
} = this.state;
|
||||
|
||||
const isExpired = (listing) => listing.bumped_at && (!listing.published) ? ((Date.now() - new Date(listing.bumped_at.toString()).getTime()) / (1000 * 60 * 60 * 24)) > 30 : false;
|
||||
const isDraft = (listing) => listing.bumped_at ? !isExpired(listing) && (!listing.published) : true;
|
||||
|
||||
const filterListings = (listingsToFilter, selectedFilter) => {
|
||||
if (selectedFilter === "Expired") {
|
||||
return listingsToFilter.filter(listing => listing.published === false)
|
||||
if (selectedFilter === "Draft") {
|
||||
return listingsToFilter.filter(listing => isDraft(listing))
|
||||
} if (selectedFilter === "Expired") {
|
||||
return listingsToFilter.filter(listing => isExpired(listing))
|
||||
} if (selectedFilter === "Active") {
|
||||
return listingsToFilter.filter(listing => listing.published === true)
|
||||
}
|
||||
return listingsToFilter
|
||||
}
|
||||
|
||||
const customSort = (a,b) => {
|
||||
if (a[sort] === null) {
|
||||
return 1
|
||||
}
|
||||
if (b[sort] === null) {
|
||||
return -1
|
||||
}
|
||||
if (a[sort] > b[sort]) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
const showListings = (selected, userListings, organizationListings, selectedFilter) => {
|
||||
let displayedListings;
|
||||
if (selected === 'user') {
|
||||
displayedListings = filterListings(userListings, selectedFilter)
|
||||
displayedListings = filterListings(userListings, selectedFilter).sort(customSort)
|
||||
return displayedListings.map(listing => <ListingRow listing={listing} />)
|
||||
}
|
||||
displayedListings = filterListings(organizationListings, selectedFilter)
|
||||
displayedListings = filterListings(organizationListings, selectedFilter).sort(customSort)
|
||||
return displayedListings.map(listing =>
|
||||
listing.organization_id === selected ? (
|
||||
<ListingRow listing={listing} />
|
||||
|
|
@ -58,13 +78,8 @@ export class ListingDashboard extends Component {
|
|||
),
|
||||
);
|
||||
};
|
||||
|
||||
const sortListings = (event) => {
|
||||
const sortedListings = listings.sort((a,b) => (a[event.target.value] > b[event.target.value]) ? -1 : 1)
|
||||
this.setState({listings: sortedListings});
|
||||
}
|
||||
|
||||
const filters = ["All", "Active", "Expired"];
|
||||
const filters = ["All", "Active", "Draft", "Expired"];
|
||||
const filterButtons = filters.map(f => (
|
||||
<span
|
||||
onClick={(event) => {this.setState( {filter:event.target.textContent} )}}
|
||||
|
|
@ -80,7 +95,7 @@ export class ListingDashboard extends Component {
|
|||
<div className="listings-dashboard-filter-buttons">
|
||||
{filterButtons}
|
||||
</div>
|
||||
<select onChange={sortListings} >
|
||||
<select onChange={(event) => {this.setState({sort: event.target.value})}}>
|
||||
<option value="created_at" selected="selected">Recently Created</option>
|
||||
<option value="bumped_at">Recently Bumped</option>
|
||||
</select>
|
||||
|
|
@ -100,10 +115,15 @@ export class ListingDashboard extends Component {
|
|||
|
||||
const listingLength = (selected, userListings, organizationListings) => {
|
||||
return selected === 'user' ? (
|
||||
<h4>Listings Made: {userListings.length}</h4>
|
||||
<h4>
|
||||
Listings Made:
|
||||
{' '}
|
||||
{userListings.length}
|
||||
</h4>
|
||||
) : (
|
||||
<h4>
|
||||
Listings Made:{' '}
|
||||
Listings Made:
|
||||
{' '}
|
||||
{
|
||||
organizationListings.filter(
|
||||
listing => listing.organization_id === selected,
|
||||
|
|
@ -115,10 +135,15 @@ export class ListingDashboard extends Component {
|
|||
|
||||
const creditCount = (selected, userCreds, organizations) => {
|
||||
return selected === 'user' ? (
|
||||
<h4>Credits Available: {userCredits}</h4>
|
||||
<h4>
|
||||
Credits Available:
|
||||
{' '}
|
||||
{userCreds}
|
||||
</h4>
|
||||
) : (
|
||||
<h4>
|
||||
Credits Available:{' '}
|
||||
Credits Available:
|
||||
{' '}
|
||||
{organizations.find(org => org.id === selected).unspent_credits_count}
|
||||
</h4>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import BodyMarkdown from './elements/bodyMarkdown';
|
|||
import Categories from './elements/categories';
|
||||
import Tags from './elements/tags';
|
||||
import OrgSettings from './elements/orgSettings';
|
||||
import ContactViaConnect from './elements/contactViaConnect';
|
||||
import ExpireDate from './elements/expireDate';
|
||||
|
||||
export default class ListingForm extends Component {
|
||||
|
|
@ -29,6 +30,7 @@ export default class ListingForm extends Component {
|
|||
categoriesForDetails: this.categoriesForDetails,
|
||||
organizations,
|
||||
organizationId: null, // change this for /edit later
|
||||
contactViaConnect: this.listing.contact_via_connect || 'checked',
|
||||
expireDate: this.listing.expires_at || '',
|
||||
};
|
||||
}
|
||||
|
|
@ -49,40 +51,22 @@ export default class ListingForm extends Component {
|
|||
categoriesForSelect,
|
||||
organizations,
|
||||
organizationId,
|
||||
contactViaConnect,
|
||||
expireDate,
|
||||
} = this.state;
|
||||
const orgArea =
|
||||
organizations && organizations.length > 0 ? (
|
||||
<OrgSettings
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={this.handleOrgIdChange}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
const selectOrg = ((organizations && organizations.length > 0) ? <OrgSettings organizations={organizations} organizationId={organizationId} onToggle={this.handleOrgIdChange} /> : '');
|
||||
|
||||
if (id === null) {
|
||||
return (
|
||||
<div>
|
||||
<Title defaultValue={title} onChange={linkState(this, 'title')} />
|
||||
<BodyMarkdown
|
||||
defaultValue={bodyMarkdown}
|
||||
onChange={linkState(this, 'bodyMarkdown')}
|
||||
/>
|
||||
<Categories
|
||||
categoriesForSelect={categoriesForSelect}
|
||||
categoriesForDetails={categoriesForDetails}
|
||||
onChange={linkState(this, 'category')}
|
||||
category={category}
|
||||
/>
|
||||
<Tags
|
||||
defaultValue={tagList}
|
||||
category={category}
|
||||
onInput={linkState(this, 'tagList')}
|
||||
/>
|
||||
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
|
||||
<Categories categoriesForSelect={categoriesForSelect} categoriesForDetails={categoriesForDetails} onChange={linkState(this, 'category')} category={category} />
|
||||
<Tags defaultValue={tagList} category={category} onInput={linkState(this, 'tagList')} />
|
||||
<ExpireDate defaultValue={expireDate} onChange={linkState(this, 'expireDate')} />
|
||||
{orgArea}
|
||||
{/* add contact via connect checkbox later */}
|
||||
{selectOrg}
|
||||
<ContactViaConnect defaultValue={contactViaConnect} onChange={linkState(this, 'contactViaConnect')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -95,8 +79,8 @@ export default class ListingForm extends Component {
|
|||
onChange={linkState(this, 'bodyMarkdown')}
|
||||
/>
|
||||
<Tags defaultValue={tagList} onInput={linkState(this, 'tagList')} />
|
||||
{orgArea}
|
||||
{/* add contact via connect checkbox later */}
|
||||
{selectOrg}
|
||||
<ContactViaConnect checked={contactViaConnect} onChange={linkState(this, 'contactViaConnect')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,8 +260,10 @@ export class Listings extends Component {
|
|||
index.search(query, filterObject).then(function searchDone(content) {
|
||||
const fullListings = listings;
|
||||
content.hits.forEach(listing => {
|
||||
if (!listings.map(l => l.id).includes(listing.id)) {
|
||||
fullListings.push(listing);
|
||||
if (listing.bumped_at) {
|
||||
if (!listings.map(l => l.id).includes(listing.id)) {
|
||||
fullListings.push(listing);
|
||||
}
|
||||
}
|
||||
});
|
||||
t.setState({
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ class ClassifiedListing < ApplicationRecord
|
|||
"/listings/#{category}/#{slug}"
|
||||
end
|
||||
|
||||
def natural_expiration_date
|
||||
(bumped_at || created_at) + 30.days
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def evaluate_markdown
|
||||
|
|
|
|||
|
|
@ -11,6 +11,14 @@ class ClassifiedListingPolicy < ApplicationPolicy
|
|||
user.org_member?(record.organization_id)
|
||||
end
|
||||
|
||||
def delete_confirm?
|
||||
update?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
update?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_is_author?
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@
|
|||
</div>
|
||||
<% end %>
|
||||
<div id="listingform-data"
|
||||
data-listing="<%= classified_listing.to_json(only: %i[id title body_markdown category cached_tag_list]) %>"
|
||||
data-organizations="<%= @organizations.to_json(only: %i[id name]) %>"
|
||||
data-categories-for-select="<%= ClassifiedListing.select_options_for_categories.to_json %>"
|
||||
data-categories-for-details="<%= ClassifiedListing.categories_available.transform_values { |value_hash| value_hash.except(:cost) }.values.to_json %>"
|
||||
>
|
||||
data-listing="<%= classified_listing.to_json(only: %i[id title body_markdown category cached_tag_list]) %>"
|
||||
data-organizations="<%= @organizations.to_json(only: %i[id name]) %>"
|
||||
data-categories-for-select="<%= ClassifiedListing.select_options_for_categories.to_json %>"
|
||||
data-categories-for-details="<%= ClassifiedListing.categories_available.transform_values { |value_hash| value_hash.except(:cost) }.values.to_json %>">
|
||||
<div style="height: 745px;">
|
||||
<div class="field">
|
||||
<%= form.label "title" %>
|
||||
|
|
@ -59,13 +58,13 @@
|
|||
<%= form.label "location", "Location (If applicable for events, etc.)" %>
|
||||
<%= form.text_field "location", placeholder: "32 characters max, plain text" %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= form.label "contact_via_connect", "Allow Users to Message Me Via In-App Chat (DEV Connect)" %>
|
||||
<%= form.check_box "contact_via_connect", checked: true %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<br />
|
||||
<p>You will not be charged credits to save a draft.</p>
|
||||
<%= form.button "SAVE DRAFT", type: "submit", name: "classified_listing[action]", class: "cta cta-main-listing-form cta-draft", value: "draft" %>
|
||||
<% if @credits.size > 0 || (@organizations.present? && @organizations.sum(:unspent_credits_count) > 0) %>
|
||||
<%= form.submit "CREATE LISTING", class: "cta cta-main-listing-form" %>
|
||||
<%= form.submit "PUBLISH LISTING", class: "cta cta-main-listing-form" %>
|
||||
<% else %>
|
||||
<p>You need at least one credit to create a listing.</p>
|
||||
<a href="/credits/new" class="cta cta-main-listing-form" data-no-instant>Purchase Credits Now</a>
|
||||
|
|
@ -84,10 +83,10 @@
|
|||
<div class="listings-current-credits-inner">
|
||||
<select id="org-credits-select" class="org-credits-select">
|
||||
<% @organizations.each do |org| %>
|
||||
<option value="<%= org.id %>" data-credits="<%= org.unspent_credits_count %>"><%= org.name %></option>
|
||||
<option value="<%= org.id %>" data-credits="<%= org.credits.unspent.size %>"><%= org.name %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
has <span id="org-credits-number"><%= @organizations.first.unspent_credits_count %></span> credits -
|
||||
has <span id="org-credits-number"><%= @organizations.first.credits.unspent.size %></span> credits -
|
||||
<a id="org-credits-purchase-link" target="_blank" rel="noopener noreferrer" href="/credits/purchase?organization_id=<%= @organizations.first.id %>" data-no-instant>Buy More</a>
|
||||
</div>
|
||||
<%= javascript_pack_tag "orgCreditsSelector", defer: true %>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<% title "Listings Dashboard" %>
|
||||
|
||||
<div id="classifieds-listings-dashboard" data-listings="<%= @classified_listings.to_json(
|
||||
only: %i[title tag_list created_at expires_at bumped_at updated_at category id user_id slug organization_id location published],
|
||||
include: {
|
||||
author: { only: %i[username name], methods: %i[username profile_image_90] },
|
||||
},
|
||||
)%>" data-usercredits="<%= @user_credits %>"
|
||||
only: %i[title tag_list created_at expires_at bumped_at updated_at category id user_id slug organization_id location published],
|
||||
include: {
|
||||
author: { only: %i[username name], methods: %i[username profile_image_90] },
|
||||
},
|
||||
)%>" data-usercredits="<%= @user_credits %>"
|
||||
data-orglistings="<%= @org_listings.to_json(
|
||||
only: %i[title tag_list created_at expires_at bumped_at updated_at category id user_id slug organization_id location published],
|
||||
include: {
|
||||
|
|
|
|||
18
app/views/classified_listings/delete_confirm.html.erb
Normal file
18
app/views/classified_listings/delete_confirm.html.erb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<%= javascript_include_tag "application" %>
|
||||
|
||||
<div class="container delete-confirm">
|
||||
<h4><%= @classified_listing.title %></h4>
|
||||
<h1>Are you sure you want to delete this listing?</h1>
|
||||
|
||||
<h2>
|
||||
You cannot undo this action, perhaps you just want to
|
||||
<a data-no-instant href="/listings/<%= @classified_listing.id %>/edit">
|
||||
<%= @classified_listing.published ? "unpublish" : "edit" %>
|
||||
</a>
|
||||
instead?
|
||||
</h2>
|
||||
|
||||
<h2>
|
||||
<a class="delete-link" data-no-instant rel="nofollow" data-method="delete" href="/listings/<%= @classified_listing.id %>">DELETE</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -1,31 +1,13 @@
|
|||
<div class="classifieds-container">
|
||||
<% title "Edit Listing" %>
|
||||
|
||||
<div class="classifieds-container classifieds-form-container">
|
||||
<%= form_for(@classified_listing) do |f| %>
|
||||
<header>
|
||||
<a href="/listings/dashboard" class="listings-back-button">< return to listings dashboard</a>
|
||||
<h2>You can only edit title/body/tags of drafts or within the first 24 hours of listing or bumping</h2>
|
||||
</header>
|
||||
<div class="classified-form-inner">
|
||||
<p>
|
||||
You can bump your listing for the same price as the original listing
|
||||
</p>
|
||||
<h2>
|
||||
Last Published or Bumped: <%= time_ago_in_words @classified_listing.bumped_at %> ago
|
||||
</h2>
|
||||
<% if (@classified_listing.bumped_at + 30.days) < Date.today %>
|
||||
<h4>
|
||||
(Expired <%= time_ago_in_words (@classified_listing.bumped_at + 30.days) %> ago)
|
||||
</h4>
|
||||
<% else %>
|
||||
<h4>
|
||||
(Expires in <%= time_ago_in_words (@classified_listing.bumped_at + 30.days) %>)
|
||||
</h4>
|
||||
<% end %>
|
||||
<input type="hidden" name="classified_listing[action]" value="bump" />
|
||||
<%= f.submit "Bump Listing", class: "cta cta-main-listing-form" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for(@classified_listing) do |f| %>
|
||||
<div class="classified-form-inner">
|
||||
<h2>
|
||||
<em>You can only edit title/body/tags within the first 24 hours of listing or bumping</em>
|
||||
</h2>
|
||||
<% if @classified_listing.bumped_at > 24.hours.ago %>
|
||||
<% if (@classified_listing.bumped_at && @classified_listing.bumped_at > 24.hours.ago) || @classified_listing.updated_at && !@classified_listing.published %>
|
||||
<div class="field">
|
||||
<%= f.label "title", "Title" %>
|
||||
<%= f.text_field "title", maxlength: 128, placeholder: "128 characters max, plain text" %>
|
||||
|
|
@ -44,27 +26,54 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<%= f.label "expires_at", "Custom Expire Date" %>
|
||||
<%= f.date_field "expires_at", min: Date.tomorrow, max: @classified_listing.bumped_at + 30.days %>
|
||||
<%= f.date_field "expires_at", min: Date.tomorrow, max: @classified_listing.natural_expiration_date %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= f.label "contact_via_connect", "Allow Users to Message Me Via In-App Chat (DEV Connect)" %>
|
||||
<%= f.check_box "contact_via_connect" %>
|
||||
</div>
|
||||
<%= f.submit "Update Listings Info", class: "cta cta-main-listing-form" %>
|
||||
<% if @classified_listing.published %>
|
||||
<%= f.submit "Update Listings Info", class: "cta cta-main-listing-form" %>
|
||||
<% else %>
|
||||
<%= f.submit "Update Draft", class: "cta cta-main-listing-form cta-draft" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @classified_listing.bumped_at %>
|
||||
<%= form_for(@classified_listing) do |f| %>
|
||||
<header>
|
||||
<a href="<%= @classified_listing.path %>" class="listings-back-button" style="font-size: 1.1em;">Last Published/Bumped: <%= time_ago_in_words @classified_listing.bumped_at %> ago</a>
|
||||
<h2>You can bump your listing for the same price as the original listing</h2>
|
||||
</header>
|
||||
<div class="classified-form-inner">
|
||||
<% if (@classified_listing.natural_expiration_date) < Date.today %>
|
||||
<h4>
|
||||
(Expired <%= time_ago_in_words @classified_listing.natural_expiration_date %> ago)
|
||||
</h4>
|
||||
<% else %>
|
||||
<h4>
|
||||
(Expires in <%= time_ago_in_words @classified_listing.natural_expiration_date %>)
|
||||
</h4>
|
||||
<% end %>
|
||||
<input type="hidden" name="classified_listing[action]" value="bump" />
|
||||
<%= f.submit "Bump Listing", class: "cta cta-main-listing-form" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= form_for(@classified_listing) do |f| %>
|
||||
<div class="classified-form-inner">
|
||||
<h2>
|
||||
Published: <%= @classified_listing.published %>
|
||||
</h2>
|
||||
<% unless @classified_listing.published %>
|
||||
<h2 style="text-align: center; margin-top: 15px;">
|
||||
This listing is not published
|
||||
</h2>
|
||||
<% end %>
|
||||
<% if @classified_listing.published == false %>
|
||||
<%# <input type="hidden" name="classified_listing[action]" value="publish" />
|
||||
<%= f.submit "Publish Listing", class: "classified-listings-publish cta-main-listing-form" %>
|
||||
<%# <% else %>
|
||||
<input type="hidden" name="classified_listing[action]" value="publish" />
|
||||
<%= f.submit "Publish Listing", class: "cta classified-listings-publish cta-main-listing-form" %>
|
||||
<% else %>
|
||||
<input type="hidden" name="classified_listing[action]" value="unpublish" />
|
||||
<%= f.submit "Unpublish Listing", class: "classified-listings-unpublish cta-main-listing-form" %>
|
||||
<%= f.submit "Unpublish Listing", class: "cta classified-listings-unpublish cta-main-listing-form" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<% title "New Listing" %>
|
||||
|
||||
<div class="classifieds-container" id="classifieds-container">
|
||||
<%= render 'form', classified_listing: @classified_listing %>
|
||||
<div class="classifieds-container classifieds-form-container" id="classifieds-container">
|
||||
<%= render "form", classified_listing: @classified_listing %>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@
|
|||
<p>
|
||||
<a href="<%= new_classified_listing_path %>">Create a Listing</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/listings/dashboard">Go to Listings Dashboard</a>
|
||||
</p>
|
||||
|
||||
<% if @organizations.present? %>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
Upload a Video 🎥
|
||||
</a>
|
||||
<% end %>
|
||||
<a class="video-upload-cta cta" href="/listings/dashboard" data-no-instant>
|
||||
Create/Manage Listings
|
||||
</a>
|
||||
<%= select_tag "dashhboard_sort", options_for_select(sort_options, params[:sort]) %>
|
||||
<% if @articles.any? {|article| article.archived } %>
|
||||
<%= link_to "Show archived", "javascript:;", id: "toggleArchivedLink" %>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<div class="grid-item"><%= listing.category %></div>
|
||||
<div class="grid-item"><%= listing.cached_tag_list %></div>
|
||||
<div class="grid-item"><%= listing.published ? "Yes" : "No" %></div>
|
||||
<div class="grid-item"><%= time_ago_in_words(listing.bumped_at) %> ago</div>
|
||||
<div class="grid-item"><%= listing.bumped_at ? time_ago_in_words(listing.bumped_at) + " ago" : "Draft" %></div>
|
||||
</div>
|
||||
<div class="buffer-area" style="text-align: left">
|
||||
<br>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ Rails.application.routes.draw do
|
|||
resources :tag_adjustments, only: %i[create destroy]
|
||||
resources :rating_votes, only: [:create]
|
||||
resources :page_views, only: %i[create update]
|
||||
resources :classified_listings, path: :listings, only: %i[index new create edit update delete dashboard]
|
||||
resources :classified_listings, path: :listings, only: %i[index new create edit update destroy dashboard]
|
||||
resources :credits, only: %i[index new create] do
|
||||
get "purchase", on: :collection, to: "credits#new"
|
||||
end
|
||||
|
|
@ -177,6 +177,8 @@ Rails.application.routes.draw do
|
|||
get "/listings/:category/:slug" => "classified_listings#index", as: :classified_listing_slug
|
||||
get "/listings/:category/:slug/:view" => "classified_listings#index",
|
||||
constraints: { view: /moderate/ }
|
||||
get "/listings/:category/:slug/delete_confirm" => "classified_listings#delete_confirm"
|
||||
delete "/listings/:category/:slug" => "classified_listings#destroy"
|
||||
get "/notifications/:filter" => "notifications#index"
|
||||
get "/notifications/:filter/:org_id" => "notifications#index"
|
||||
get "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#show"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
}
|
||||
}
|
||||
end
|
||||
let(:draft_params) do
|
||||
{
|
||||
classified_listing: {
|
||||
title: "this a draft",
|
||||
body_markdown: "something draft",
|
||||
category: "cfp",
|
||||
tag_list: "",
|
||||
contact_via_connect: true,
|
||||
action: "draft"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
describe "GET /listings" do
|
||||
let(:listing) { create(:classified_listing, user: user) }
|
||||
|
|
@ -56,6 +68,11 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
describe "GET /listings/new" do
|
||||
before { sign_in user }
|
||||
|
||||
it "shows the option to create draft" do
|
||||
get "/listings/new"
|
||||
expect(response.body).to include "You will not be charged credits to save a draft."
|
||||
end
|
||||
|
||||
context "when the user has no credits" do
|
||||
it "shows the proper messages" do
|
||||
get "/listings/new"
|
||||
|
|
@ -154,6 +171,11 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
expect(response.body).to redirect_to("/credits")
|
||||
end
|
||||
|
||||
it "redirects to /listings/dashboard for listing draft" do
|
||||
post "/listings", params: draft_params
|
||||
expect(response).to redirect_to "/listings/dashboard"
|
||||
end
|
||||
|
||||
it "redirects to /listings" do
|
||||
post "/listings", params: listing_params
|
||||
expect(response).to redirect_to "/listings"
|
||||
|
|
@ -165,6 +187,16 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
expect(user.credits.spent.size).to eq(listing_cost)
|
||||
end
|
||||
|
||||
it "creates a listing draft under the org" do
|
||||
org_admin = create(:user, :org_admin)
|
||||
org_id = org_admin.organizations.first.id
|
||||
Credit.create(organization_id: org_id)
|
||||
draft_params[:classified_listing][:organization_id] = org_id
|
||||
sign_in org_admin
|
||||
post "/listings", params: draft_params
|
||||
expect(ClassifiedListing.first.organization_id).to eq org_id
|
||||
end
|
||||
|
||||
it "creates a listing under the org" do
|
||||
org_admin = create(:user, :org_admin)
|
||||
org_id = org_admin.organizations.first.id
|
||||
|
|
@ -175,6 +207,12 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
expect(ClassifiedListing.first.organization_id).to eq org_id
|
||||
end
|
||||
|
||||
it "does not create a listing draft for an org not belonging to the user" do
|
||||
org = create(:organization)
|
||||
draft_params[:classified_listing][:organization_id] = org.id
|
||||
expect { post "/listings", params: draft_params }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
|
||||
it "does not create a listing for an org not belonging to the user" do
|
||||
org = create(:organization)
|
||||
listing_params[:classified_listing][:organization_id] = org.id
|
||||
|
|
@ -188,6 +226,14 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
expect(spent_credit.spent_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "creates listing draft and does not subtract credits" do
|
||||
allow(Credits::Buyer).to receive(:call).and_raise(ActiveRecord::Rollback)
|
||||
expect do
|
||||
post "/listings", params: draft_params
|
||||
end.to change(ClassifiedListing, :count).by(1).
|
||||
and change(user.credits.spent, :size).by(0)
|
||||
end
|
||||
|
||||
it "does not create a listing or subtract credits if the purchase does not go through" do
|
||||
allow(Credits::Buyer).to receive(:call).and_raise(ActiveRecord::Rollback)
|
||||
expect do
|
||||
|
|
@ -209,11 +255,15 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
|
||||
describe "PUT /listings/:id" do
|
||||
let(:listing) { create(:classified_listing, user: user) }
|
||||
let(:listing_draft) { create(:classified_listing, user: user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:org_listing) { create(:classified_listing, user: user, organization: organization) }
|
||||
let(:org_listing_draft) { create(:classified_listing, user: user, organization: organization) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
listing_draft.update_columns(bumped_at: nil, published: false)
|
||||
org_listing_draft.update_columns(bumped_at: nil, published: false)
|
||||
end
|
||||
|
||||
context "when the bump action is called" do
|
||||
|
|
@ -272,5 +322,156 @@ RSpec.describe "ClassifiedListings", type: :request do
|
|||
expect(org_listing.reload.bumped_at >= previous_bumped_at).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the publish action is called" do
|
||||
let(:params) { { classified_listing: { action: "publish" } } }
|
||||
|
||||
it "publishes a draft and charges user credits if first publish" do
|
||||
cost = ClassifiedListing.cost_by_category(listing_draft.category)
|
||||
create_list(:credit, cost, user: user)
|
||||
expect do
|
||||
put "/listings/#{listing_draft.id}", params: params
|
||||
end.to change(user.credits.spent, :size).by(cost)
|
||||
end
|
||||
|
||||
it "publishes a draft and ensures published column is true" do
|
||||
cost = ClassifiedListing.cost_by_category(listing_draft.category)
|
||||
create_list(:credit, cost, user: user)
|
||||
put "/listings/#{listing_draft.id}", params: params
|
||||
expect(listing_draft.reload.published).to eq(true)
|
||||
end
|
||||
|
||||
it "publishes an org draft and charges org credits if first publish" do
|
||||
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
|
||||
create_list(:credit, cost, organization: organization)
|
||||
expect do
|
||||
put "/listings/#{org_listing_draft.id}", params: params
|
||||
end.to change(organization.credits.spent, :size).by(cost)
|
||||
end
|
||||
|
||||
it "publishes an org draft and ensures published column is true" do
|
||||
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
|
||||
create_list(:credit, cost, organization: organization)
|
||||
put "/listings/#{org_listing_draft.id}", params: params
|
||||
expect(org_listing_draft.reload.published).to eq(true)
|
||||
end
|
||||
|
||||
it "publishes a draft that was charged and is within 30 days of bump doesn't charge credits" do
|
||||
listing.update_column(:published, false)
|
||||
expect do
|
||||
put "/listings/#{listing.id}", params: params
|
||||
end.to change(user.credits.spent, :size).by(0)
|
||||
end
|
||||
|
||||
it "publishes a draft that was charged and is within 30 days of bump and successfully sets published as true" do
|
||||
listing.update_column(:published, false)
|
||||
put "/listings/#{listing.id}", params: params
|
||||
expect(listing.reload.published).to eq(true)
|
||||
end
|
||||
|
||||
it "fails to publish draft and doesn't charge credits" do
|
||||
expect do
|
||||
put "/listings/#{listing_draft.id}", params: params
|
||||
end.to change(user.credits.spent, :size).by(0)
|
||||
end
|
||||
|
||||
it "fails to publish draft and published remains false" do
|
||||
put "/listings/#{listing_draft.id}", params: params
|
||||
expect(listing_draft.reload.published).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the unpublish action is called" do
|
||||
let(:params) { { classified_listing: { action: "unpublish" } } }
|
||||
|
||||
it "unpublishes a published listing" do
|
||||
put "/listings/#{listing.id}", params: params
|
||||
expect(listing.reload.published).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DEL /listings/:id" do
|
||||
let!(:listing) { create(:classified_listing, user: user) }
|
||||
let!(:listing_draft) { create(:classified_listing, user: user, bumped_at: nil, published: false) }
|
||||
let(:organization) { create(:organization) }
|
||||
let!(:org_listing) { create(:classified_listing, user: user, organization: organization) }
|
||||
let!(:org_listing_draft) { create(:classified_listing, user: user, organization: organization, bumped_at: nil, published: false) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
context "when deleting draft" do
|
||||
it "redirect to dashboard" do
|
||||
delete "/listings/#{listing_draft.id}"
|
||||
expect(response).to redirect_to("/listings/dashboard")
|
||||
end
|
||||
|
||||
it "have status 302 found" do
|
||||
delete "/listings/#{listing_draft.id}"
|
||||
expect(response).to have_http_status(:found)
|
||||
end
|
||||
|
||||
it "decrease total listings count by 1" do
|
||||
expect do
|
||||
delete "/listings/#{listing_draft.id}"
|
||||
end.to change(ClassifiedListing, :count).by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when deleting listing" do
|
||||
it "redirect to dashboard" do
|
||||
delete "/listings/#{listing.id}"
|
||||
expect(response).to redirect_to("/listings/dashboard")
|
||||
end
|
||||
|
||||
it "have status 302 found" do
|
||||
delete "/listings/#{listing.id}"
|
||||
expect(response).to have_http_status(:found)
|
||||
end
|
||||
|
||||
it "decrease total listings count by 1" do
|
||||
expect do
|
||||
delete "/listings/#{listing.id}"
|
||||
end.to change(ClassifiedListing, :count).by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when deleting org listing" do
|
||||
it "redirect to dashboard" do
|
||||
delete "/listings/#{org_listing_draft.id}"
|
||||
expect(response).to redirect_to("/listings/dashboard")
|
||||
end
|
||||
|
||||
it "have status 302 found" do
|
||||
delete "/listings/#{org_listing_draft.id}"
|
||||
expect(response).to have_http_status(:found)
|
||||
end
|
||||
|
||||
it "decrease total listings count by 1" do
|
||||
expect do
|
||||
delete "/listings/#{org_listing_draft.id}"
|
||||
end.to change(ClassifiedListing, :count).by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when deleting org listing" do
|
||||
it "redirect to dashboard" do
|
||||
delete "/listings/#{org_listing.id}"
|
||||
expect(response).to redirect_to("/listings/dashboard")
|
||||
end
|
||||
|
||||
it "have status 302 found" do
|
||||
delete "/listings/#{org_listing.id}"
|
||||
expect(response).to have_http_status(:found)
|
||||
end
|
||||
|
||||
it "decrease total listings count by 1" do
|
||||
expect do
|
||||
delete "/listings/#{org_listing.id}"
|
||||
end.to change(ClassifiedListing, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue