Crayonsification of Listings form (#10429)

* i dont know how to test

* fix i guess

* layout update

* layout updates

* edit

* edit listing

* fix

* tags field

* specs|

* Update app/javascript/listings/__tests__/ContactViaConnect.test.jsx

Co-authored-by: Fernando Valverde <fernando@visualcosita.com>

Co-authored-by: Fernando Valverde <fernando@visualcosita.com>
This commit is contained in:
ludwiczakpawel 2020-09-30 09:14:48 +02:00 committed by GitHub
parent d7976a75d6
commit 90d04a73db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 399 additions and 462 deletions

View file

@ -82,6 +82,18 @@
width: 100%;
}
}
&--inverted {
.crayons-layout__sidebar-right {
order: -1;
}
@media (min-width: $breakpoint-m) {
--layout: var(--layout-content-width) var(--layout-sidebar-left-width);
.crayons-layout__sidebar-right {
order: 1;
}
}
}
}
.crayons-layout--3-cols {

View file

@ -1,230 +1,6 @@
@import 'variables';
@import 'mixins';
.listings-container {
&.listings-form-container {
form {
margin: 60px auto 0px;
width: 100%;
max-width: 580px;
background: var(--card-bg);
box-sizing: border-box;
border: 1px solid var(--card-color-tertiary);
@media screen and (min-width: 580px) {
border-radius: 8px;
margin: 20px auto 20px;
}
* {
box-sizing: border-box;
}
header {
padding: 25px 22px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.listing-form-inner,
.listings-form-inner {
padding: 0px 15px 40px;
@media screen and (min-width: 580px) {
padding: 0px 15px 18px;
}
}
.listing-back-button {
font-size: 0.8em;
margin: 2px 0px 8px;
display: block;
}
h2 {
font-size: 1.45em;
color: var(--accent-brand);
margin: 0px;
}
.listing-errors {
background: $red;
color: $black;
padding: 20px;
border-radius: 3px;
h2 {
color: $black;
}
}
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: 10px 7px;
border-radius: 3px;
border: 1px solid var(--form-border);
background: var(--form-bg);
color: var(--body-color);
&[type='checkbox'] {
width: 100px;
}
}
textarea {
width: 100%;
height: 300px;
padding: 5px;
font-size: 17px;
border-radius: 3px;
background: var(--form-bg);
color: var(--body-color);
border: 1px solid var(--form-border);
}
select {
width: 100%;
font-size: 20px;
}
details {
cursor: pointer;
font-size: 0.8em;
padding: 8px 0px;
color: var(--card-color-tertiary);
}
.cta-main-listing-form {
border: 0px;
padding: 12px 0px;
border-radius: 3px;
background-color: $dark-purple;
background-image: linear-gradient(
141deg,
$dark-purple 0%,
#1f89db 51%,
#2981e5 75%
);
border-radius: 100px;
font-size: 1.4em;
color: white;
margin-top: 20px;
width: 380px;
max-width: 90%;
margin: 35px auto;
display: block;
text-align: center;
@include themeable(
box-shadow,
theme-container-box-shadow,
2px 2px 8px darken($light-medium-gray, 5%)
);
&.cta-draft {
color: $black;
background: $yellow;
}
}
.listings-current-credits {
position: fixed;
bottom: 0px;
left: 0px;
right: 0px;
padding: 18px 0px;
background-color: #aecfef;
background-image: linear-gradient(
141deg,
#aecfef 0%,
#ddeeff 51%,
#c8ebf1 75%
);
color: #32325d;
text-align: center;
z-index: 20;
font-size: 0.85em;
.listings-current-credits-inner {
display: inline-block;
margin: 20px;
select {
width: initial;
}
}
a {
text-decoration: underline;
display: inline-block;
color: var(--link-brand-color);
}
@media screen and (min-width: 580px) {
font-size: 1.1em;
}
}
.listings-unpublish {
background: $red;
}
p {
font-size: 0.8em;
color: var(--card-color-tertiary);
}
}
}
}
.listingform__bodymarkdown {
width: 100%;
height: 300px;
padding: 5px;
font-size: 17px;
border-radius: 3px;
background: var(--card-bg);
margin-top: 10px;
color: var(--body-color);
border: 1px solid $light-medium-gray;
}
.listingform__tagsoptions {
border: 1px solid var(--card-color-tertiary);
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.listingform__tagoptionrow {
padding: 10px;
cursor: pointer;
position: relative;
&:hover {
background: lighten($green, 27%);
color: $black;
}
}
.listingform__tagsoptionsbottomrow,
.listingform__tagsoptionrulesbutton,
.listingform__tagrules--inactive {
display: none; //not yet fully functional or relevant
}
.listingform__tagoptionrow--active {
background: $green;
color: $black;
&:hover {
background: darken($green, 10%);
}
}
.listingform__label {
width: 100%;
display: inline-block;
font-size: 0.7em;
margin: 8px 0px 3px 0px;
border-radius: 3px;
padding: 6px 0px;
font-weight: bold;
}
.listings-modal-background {
z-index: 15;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
user-select: none;
}
.dashboard-listings-container {
width: 800px;
max-width: 96%;

View file

@ -261,10 +261,8 @@
}
&__tagname {
font-family: var(--ff-monospace);
color: var(--base-90);
letter-spacing: -0.05em;
font-weight: bold;
font-weight: var(--fw-medium);
}
&__tagoptionrow {

View file

@ -35,3 +35,59 @@
color: var(--card-color-tertiary);
}
}
// New/Edit Listing form
// Tags dropdown
// TODO: componetize styling..
.listingform {
&__tagsoptions {
@include generate-box(
$level: 2,
$bg: var(--card-bg),
$border: var(--card-color),
$color: var(--card-color)
);
position: absolute;
left: 0;
right: 0;
top: calc(100%);
z-index: 100;
padding: var(--su-1);
left: calc(var(--su-1) * -1);
right: calc(var(--su-1) * -1);
}
&__tagname {
color: var(--base-90);
font-weight: var(--fw-medium);
}
&__tagoptionrow {
cursor: pointer;
color: var(--link-color);
padding: var(--su-3);
&:hover {
background: var(--link-bg-hover);
.crayons-tag {
color: var(--link-color-hover);
}
}
&--active {
background: var(--link-bg-hover);
.crayons-article-form__tagname {
color: var(--link-color-hover);
}
}
}
&__tagrules,
&__tagsoptionrulesbutton,
&__tagsoptionsbottomrow,
&__tagrules--inactive {
display: none;
}
}

View file

@ -29,7 +29,7 @@ describe('<BodyMarkdown />', () => {
const textArea = getByLabelText('Body Markdown');
expect(textArea.getAttribute('placeholder')).toEqual(
'400 characters max, 12 line break max, no images allowed, *markdown is encouraged*',
'...',
);
expect(textArea.getAttribute('maxLength')).toEqual('400');
expect(textArea.value).toEqual(defaultValue);

View file

@ -16,28 +16,22 @@ describe('<ContactViaConnect />', () => {
it('should render a checked check box opting in to open DMs', () => {
const onChange = jest.fn();
const { queryByLabelText } = render(
const { getByText } = render(
<ContactViaConnect onChange={onChange} checked />,
);
expect(
queryByLabelText(
'Allow Users to Message Me Via In-App Chat (DEV Connect)',
),
).toBeDefined();
expect(getByText('Allow Users to message me via Connect.')).toBeDefined();
});
it('should fire a change event when clicking the checkbox', () => {
const onChange = jest.fn();
const { getByLabelText } = render(
const { getByText } = render(
<ContactViaConnect onChange={onChange} checked />,
);
const checkbox = getByLabelText(
'Allow Users to Message Me Via In-App Chat (DEV Connect)',
);
const checkbox = getByText('Allow Users to message me via Connect.');
fireEvent.input(checkbox, { target: { checked: true } });
fireEvent.click(checkbox);
expect(onChange).toHaveBeenCalledTimes(1);
});

View file

@ -2,19 +2,23 @@ import { h } from 'preact';
import PropTypes from 'prop-types';
const BodyMarkdown = ({ onChange, defaultValue }) => (
<div className="field">
<label className="listingform__label" htmlFor="body_markdown">
<div className="crayons-field">
<label className="crayons-field__label" htmlFor="body_markdown">
Body Markdown
<textarea
className="listingform__input listingform__bodymarkdown"
id="body_markdown"
name="listing[body_markdown]"
maxLength="400"
placeholder="400 characters max, 12 line break max, no images allowed, *markdown is encouraged*"
value={defaultValue}
onInput={onChange}
/>
</label>
<textarea
className="crayons-textfield"
id="body_markdown"
name="listing[body_markdown]"
maxLength="400"
placeholder="..."
value={defaultValue}
onInput={onChange}
/>
<p className="crayons-field__description">
400 characters max, 12 line break max, no images allowed, *markdown is
encouraged*
</p>
</div>
);

View file

@ -20,8 +20,8 @@ class Categories extends Component {
details = () => {
const { categoriesForDetails } = this.props;
const rules = categoriesForDetails.map((category) => {
const paragraphText = `${category.name}: ${category.rules}`;
return <p>{paragraphText}</p>;
const paragraphText = <li><strong>{category.name}:</strong> {category.rules}</li>;
return <ul>{paragraphText}</ul>;
});
return (
@ -35,19 +35,21 @@ class Categories extends Component {
render() {
const { onChange } = this.props;
return (
<div className="field">
<label className="listingform__label" htmlFor="category">
Category
</label>
<select
id="category"
className="listingform__input"
name="listing[listing_category_id]"
onChange={onChange}
onBlur={onChange}
>
{this.options()}
</select>
<div>
<div className="crayons-field mb-4">
<label className="crayons-field__label" htmlFor="category">
Category
</label>
<select
id="category"
className="crayons-select"
name="listing[listing_category_id]"
onChange={onChange}
onBlur={onChange}
>
{this.options()}
</select>
</div>
{this.details()}
</div>
);

View file

@ -2,22 +2,26 @@ import { h } from 'preact';
import PropTypes from 'prop-types';
const ContactViaConnect = ({ onChange, checked }) => (
<div className="field">
<label
id="label-contact-via-connect"
className="listingform__label"
htmlFor="contact_via_connect"
>
Allow Users to Message Me Via In-App Chat (DEV Connect)
</label>
<div className="crayons-field crayons-field--checkbox">
<input
type="checkbox"
className="listingform__input listingform__contact_via_connect"
className="crayons-checkbox"
id="contact_via_connect"
name="listing[contact_via_connect]"
onInput={onChange}
checked={checked}
/>
<label
id="label-contact-via-connect"
className="crayons-field__label"
htmlFor="contact_via_connect"
>
Connect messaging
<p className="crayons-field__description">
Allow Users to message me via Connect.
</p>
</label>
</div>
);

View file

@ -10,14 +10,16 @@ const ExpireDate = ({ onChange, defaultValue }) => {
[monthFromToday] = monthFromToday.toISOString().split('T');
return (
<div className="field">
<label className="listingform__label" htmlFor="expires_at">
Custom Expire Date (if applicable for time sensitive events, deadlines,
etc.)
<div className="crayons-field">
<label className="crayons-field__label" htmlFor="expires_at">
Custom Expire Date
<p class="crayons-field__description">
If applicable for time sensitive events, deadlines, etc.
</p>
</label>
<input
type="date"
className="listingform__input"
className="crayons-textfield m:max-w-50"
id="expires_at"
name="listing[expires_at]"
value={defaultValue}

View file

@ -4,13 +4,11 @@ import { h } from 'preact';
const domId = 1;
const Title = ({ onChange, defaultValue }) => (
<div className="field">
<label className="listingform__label" htmlFor={domId}>
Title
</label>
<div className="crayons-field">
<label className="crayons-field__label" htmlFor={domId}>Title</label>
<input
type="text"
className="listingform__input"
className="crayons-textfield"
id={domId}
name="listing[title]"
maxLength="128"

View file

@ -64,24 +64,28 @@ export default class ListingForm extends Component {
const selectOrg =
organizations && organizations.length > 0 ? (
<div className="field">
<label htmlFor="organizationId">Post under an organization:</label>
<div className="crayons-field">
<label htmlFor="organizationId" className="crayons-field__label">
Post under an organization
</label>
<OrganizationPicker
name="listing[organization_id]"
id="listing_organization_id"
className="crayons-select m:max-w-50"
organizations={organizations}
organizationId={organizationId}
onToggle={this.handleOrgIdChange}
/>
<p>
<em>Posting on behalf of org spends org credits.</em>
<p className="crayons-field__description">
Posting on behalf of an organization spends the organization's
credits.
</p>
</div>
) : null;
if (id === null) {
return (
<div>
<div className="grid gap-6">
<Title defaultValue={title} onChange={linkState(this, 'title')} />
<BodyMarkdown
defaultValue={bodyMarkdown}
@ -93,16 +97,19 @@ export default class ListingForm extends Component {
onChange={linkState(this, 'category')}
category={category}
/>
<Tags
defaultValue={tagList}
category={category}
onInput={linkState(this, 'tagList')}
classPrefix="listingform"
maxTags={8}
autocomplete="off"
listing
pattern={DEFAULT_TAG_FORMAT}
/>
<div className="relative">
<Tags
defaultValue={tagList}
category={category}
onInput={linkState(this, 'tagList')}
classPrefix="listingform"
fieldClassName="crayons-textfield"
maxTags={8}
autocomplete="off"
listing
pattern={DEFAULT_TAG_FORMAT}
/>
</div>
<ExpireDate
defaultValue={expireDate}
onChange={linkState(this, 'expireDate')}

View file

@ -18,7 +18,6 @@ function loadElement() {
categoriesForDetails={categoriesForDetails}
/>,
root,
root.firstElementChild,
);
}
}

View file

@ -2,16 +2,18 @@ const orgCreditsSelect = document.getElementById('org-credits-select');
const orgCreditsNumber = document.getElementById('org-credits-number');
const orgCreditsLink = document.getElementById('org-credits-purchase-link');
orgCreditsNumber.innerText =
orgCreditsSelect.selectedOptions[0].dataset.credits;
if (orgCreditsSelect) {
orgCreditsNumber.innerText =
orgCreditsSelect.selectedOptions[0].dataset.credits;
const changeOrgCredits = event => {
const selectedOrgCreditsCount =
event.target.selectedOptions[0].dataset.credits;
const selectedOrgId = event.target.selectedOptions[0].value;
const changeOrgCredits = (event) => {
const selectedOrgCreditsCount =
event.target.selectedOptions[0].dataset.credits;
const selectedOrgId = event.target.selectedOptions[0].value;
orgCreditsNumber.innerText = selectedOrgCreditsCount;
orgCreditsLink.href = `/credits/purchase?organization_id=${selectedOrgId}`;
};
orgCreditsNumber.innerText = selectedOrgCreditsCount;
orgCreditsLink.href = `/credits/purchase?organization_id=${selectedOrgId}`;
};
orgCreditsSelect.addEventListener('change', changeOrgCredits);
orgCreditsSelect.addEventListener('change', changeOrgCredits);
}

View file

@ -430,8 +430,8 @@ class Tags extends Component {
}
return (
<div className={`${classPrefix}__tagswrapper`}>
{listing && <label htmlFor="Tags">Tags</label>}
<div className={`${classPrefix}__tagswrapper crayons-field`}>
{listing && <label htmlFor="Tags" class="crayons-field__label">Tags</label>}
<input
data-testid="tag-input"
aria-label="Post Tags"

View file

@ -1,9 +1,5 @@
<%= form_with(model: listing, local: true) do |form| %>
<header>
<a href="/listings/dashboard" class="listings-back-button">&lt; return to listings dashboard</a>
<h2>Create a Community Listing</h2>
</header>
<div class="listing-form-inner">
<div class="crayons-card p-6 mb-6 grid gap-6">
<%= render partial: "form_errors", locals: { listing: listing } %>
<div id="listingform-data"
@ -11,23 +7,28 @@
data-organizations="<%= @organizations.to_json(only: %i[id name]) %>"
data-categories-for-select="<%= select_options_for_categories.to_json %>"
data-categories-for-details="<%= categories_available.transform_values { |value_hash| value_hash.except(:cost) }.values.to_json %>">
<div style="height: 745px;">
<div class="field">
<%= form.label "title" %>
<%= form.text_field "title", placeholder: "128 characters max, plain text" %>
</div>
<div class="field">
<%= form.label "body_markdown", "Body Markdown" %>
<%= form.text_area "body_markdown", placeholder: "400 characters max, 12 line break max, no images allowed" %>
<div class="grid gap-6">
<div class="crayons-field">
<%= form.label "title", class: "crayons-label" %>
<%= form.text_field "title", placeholder: "128 characters max, plain text", class: "crayons-textfield" %>
</div>
<div class="field" style="padding-top: 9px">
<label>Category</label>
<select>
<option>
Conference CFP (1 Credit)
</option>
</select>
<div class="crayons-field">
<%= form.label "body_markdown", "Body Markdown", class: "crayons-field__label" %>
<%= form.text_area "body_markdown", placeholder: "...", class: "crayons-textfield" %>
<p class="crayons-field__description">400 characters max, 12 line break max, no images allowed, *markdown is encouraged*</p>
</div>
<div>
<div class="crayons-field mb-4">
<label class="crayons-field__label" id="listing_category_placeholder">Category</label>
<select class="crayons-select" id="listing_category_placeholder">
<option>
Conference CFP (1 Credit)
</option>
</select>
</div>
<details>
<summary>Category details/rules</summary>
</details>
@ -37,59 +38,71 @@
SSR. By having the form field loaded on the DOM first with this view,
we prevent the screen from "jumping" once the deferred JS is loaded
and executed. %>
<div class="field">
<%= form.label "tag_list", "Tags" %>
<%= form.text_field "tag_list", placeholder: "8 tags max, comma separated, no spaces or special characters" %>
<div class="crayons-field">
<%= form.label "tag_list", "Tags", class: "crayons-field__label" %>
<%= form.text_field "tag_list", placeholder: "Add up to 8 tags...", class: "crayons-textfield" %>
</div>
<% if @organizations.present? && @organizations.size > 0 %>
<div class="field">
<%= form.label "organization_id", "Post under an organization:" %>
<%= form.select :organization_id, "None" => "None" %>
<p><em>Posting on behalf of an organization spends the organization's credits.</em></p>
<div class="crayons-field">
<label class="crayons-field__label" for="expires_at_placeholder">
Custom Expire Date
<p class="crayons-field__description">
If applicable for time sensitive events, deadlines, etc.
</p>
</label>
<input
type="date"
class="crayons-textfield m:max-w-50"
id="expires_at_placeholder" />
</div>
<% if @organizations.present? && @organizations.size > 0 %>
<div class="crayons-field">
<%= form.label "organization_id", "Post under an organization", class: "crayons-field__label" %>
<%= form.select :organization_id, { "None" => "None"}, {}, {class: "crayons-select m:max-w-50"} %>
<p class="crayons-field__description">Posting on behalf of an organization spends the organization's credits.</p>
</div>
<% end %>
<div class="crayons-field crayons-field--checkbox">
<input
type="checkbox"
class="crayons-checkbox"
id="contact_via_connect_placeholder"
checked=true />
<label class="crayons-field__label" for="contact_via_connect_placeholder">
Connect messaging
<p class="crayons-field__description">Allow Users to message me via Connect.</p>
</label>
</div>
</div>
</div>
<% unless @organizations.present? %>
<%= javascript_packs_with_chunks_tag "listingForm", defer: true %>
<% end %>
<div class="field">
<%= form.label "location", "Location (If applicable for events, etc.)" %>
<%= form.text_field "location", placeholder: "32 characters max, plain text" %>
</div>
<div class="actions">
<br />
<p>You will not be charged credits to save a draft.</p>
<%= form.button "SAVE DRAFT", type: "submit", name: "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 "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>
<% end %>
</div>
<p><em><%= community_name %> admins may modify tags and categories to ensure accuracy. We may also take down and refund inappropriate listings.</em></p>
<p><em>Listings expire 30 days after they are published or "bumped".</em></p>
<div class="listings-current-credits">
<% if @credits.size == 0 && (@organizations.blank? || @organizations.sum(:unspent_credits_count).zero?) %>
<h3>Listings Require Credits</h3>
<% end %>
<div class="listings-current-credits-inner">
You have <%= pluralize @credits.size, "credit" %> available — <a target="_blank" rel="noopener noreferrer" href="/credits/purchase" data-no-instant>Buy More</a>
</div>
<% if @organizations.present? %>
<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.credits.unspent.size %>"><%= org.name %></option>
<% end %>
</select>
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_packs_with_chunks_tag "listingForm", "orgCreditsSelector", defer: true %>
<% end %>
<% unless @organizations.present? %>
<%= javascript_packs_with_chunks_tag "listingForm", defer: true %>
<% end %>
<div class="crayons-field">
<%= form.label "location", "Location (If applicable for events, etc.)", class: "crayons-field__label" %>
<%= form.text_field "location", placeholder: "New York", class: "crayons-textfield m:max-w-50" %>
<p class="crayons-field__description">32 characters max, plain text</p>
</div>
</div>
<div class="px-4 m:px-0">
<% unless @credits.size > 0 || (@organizations.present? && @organizations.sum(:unspent_credits_count) > 0) %>
<div class="crayons-notice mb-4">You need at least one credit to create a listing.</div>
<% end %>
<div class="flex mb-4 items-center">
<% if @credits.size > 0 || (@organizations.present? && @organizations.sum(:unspent_credits_count) > 0) %>
<%= form.submit "Publish listing", class: "crayons-btn mr-2" %>
<% else %>
<a href="/credits/new" class="crayons-btn mr-2" data-no-instant>Purchase credits</a>
<% end %>
<%= form.button "Save draft", type: "submit", name: "listing[action]", class: "crayons-btn crayons-btn--secondary", value: "draft" %>
</div>
<p class="fs-s color-base-70 mb-6">You will not be charged credits to save a draft.</p>
</div>
<% end %>

View file

@ -1,6 +1,6 @@
<% if listing.errors.any? %>
<div class="listing-errors">
<h2><%= pluralize(listing.errors.count, "error") %> prohibited this listing from being saved:</h2>
<div class="crayons-notice crayons-notice--danger">
<h2 class="fs-l"><%= pluralize(listing.errors.count, "error") %> prohibited this listing from being saved:</h2>
<ul>
<% listing.errors.full_messages.each do |message| %>
<li><%= message %></li>

View file

@ -1,82 +1,111 @@
<% title "Edit Listing" %>
<div class="listings-container listings-form-container">
<%= form_for(@listing) do |f| %>
<header>
<a href="/listings/dashboard" class="listings-back-button">&lt; 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="listings-form-inner">
<%= render partial: "form_errors", locals: { listing: @listing } %>
<header class="crayons-layout crayons-layout--limited flex items-center justify-between p-2 l:p-4 l:pb-0">
<h1 class="fs-2xl s:fs-3xl">Edit Listing</h1>
</header>
<% if (@listing.bumped_at && @listing.bumped_at > 24.hours.ago) || @listing.updated_at && !@listing.published %>
<div class="field">
<%= f.label "title", "Title" %>
<%= f.text_field "title", maxlength: 128, placeholder: "128 characters max, plain text" %>
</div>
<div class="field">
<%= f.label "body_markdown", "Body Markdown" %>
<%= f.text_area "body_markdown", maxlength: 400, placeholder: "400 characters max, 12 line break max, no images allowed" %>
</div>
<div class="field">
<%= f.label "tag_list", "Tags" %>
<%= f.text_field "tag_list", value: @listing.cached_tag_list, placeholder: "8 tags max, comma separated, no spaces or special characters" %>
</div>
<div class="field">
<%= f.label "location", "Location" %>
<%= f.text_field "location", maxlength: 32, placeholder: "32 characters max, plain text" %>
</div>
<div class="field">
<%= f.label "expires_at", "Custom Expire Date" %>
<%= f.date_field "expires_at", min: Date.tomorrow, max: @listing.natural_expiration_date %>
</div>
<div class="field">
<%= f.label "contact_via_connect", "Allow Users to Message Me Via In-App Chat (#{community_name} Connect)" %>
<%= f.check_box "contact_via_connect" %>
</div>
<% if @listing.published %>
<%= f.submit "Update Listings Info", class: "cta cta-main-listing-form" %>
<main class="crayons-layout crayons-layout--2-cols crayons-layout--2-cols--inverted crayons-layout--limited">
<div class="crayons-layout__content">
<div class="crayons-card mb-6 p-6">
<%= form_for(@listing, html: { class: "grid gap-6 mb-6" }) do |f| %>
<div class="crayons-notice">You can only edit title/body/tags of drafts or within the first 24 hours of listing or bumping</div>
<%= render partial: "form_errors", locals: { listing: @listing } %>
<% if (@listing.bumped_at && @listing.bumped_at > 24.hours.ago) || @listing.updated_at && !@listing.published %>
<div class="crayons-field">
<%= f.label "title", "Title", class: "crayons-field__label" %>
<%= f.text_field "title", maxlength: 128, placeholder: "128 characters max, plain text", class: "crayons-textfield" %>
</div>
<div class="crayons-field">
<%= f.label "body_markdown", "Body Markdown", class: "crayons-field__label" %>
<%= f.text_area "body_markdown", maxlength: 400, placeholder: "...", class: "crayons-textfield" %>
<p class="crayons-field__description">400 characters max, 12 line break max, no images allowed, *markdown is encouraged*</p>
</div>
<div class="crayons-field">
<%= f.label "tag_list", "Tags", class: "crayons-field__label" %>
<%= f.text_field "tag_list", value: @listing.cached_tag_list, placeholder: "8 tags max, comma separated, no spaces or special characters", class: "crayons-textfield" %>
</div>
<div class="crayons-field">
<%= f.label "expires_at", "Custom Expire Date", class: "crayons-field__label" %>
<%= f.date_field "expires_at", min: Date.tomorrow, max: @listing.natural_expiration_date, class: "crayons-textfield m:max-w-50" %>
<p class="crayons-field__description">
If applicable for time sensitive events, deadlines, etc.
</p>
</div>
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box "contact_via_connect", class: "crayons-checkbox" %>
<label class="crayons-field__label" for="contact_via_connect">
Connect messaging
<p class="crayons-field__description">Allow Users to message me via Connect.</p>
</label>
</div>
<div class="crayons-field">
<%= f.label "location", "Location", class: "crayons-field__label" %>
<%= f.text_field "location", maxlength: 32, placeholder: "New York", class: "crayons-textfield m:max-w-50" %>
<p class="crayons-field__description">32 characters max, plain text</p>
</div>
<div>
<% if @listing.published %>
<%= f.submit "Update listing", class: "crayons-btn" %>
<% else %>
<%= f.submit "Update draft", class: "crayons-btn crayons-btn--secondary" %>
<% end %>
</div>
<% end %>
<% end %>
<%= form_for(@listing, html: { class: "pt-6 mt-6 border-t-1 border-solid border-0 border-base-20 flex items-center" }) do |f| %>
<% if @listing.published == false %>
<input type="hidden" name="listing[action]" value="publish" />
<%= f.submit "Publish listing", class: "crayons-btn mr-4" %>
<p>This listing is <strong class="color-accent-danger">not</strong> published.</p>
<% else %>
<%= f.submit "Update Draft", class: "cta cta-main-listing-form cta-draft" %>
<input type="hidden" name="listing[action]" value="unpublish" />
<%= f.submit "Unpublish listing", class: "crayons-btn crayons-btn--danger" %>
<% end %>
<% end %>
</div>
<% end %>
<% if @listing.bumped_at %>
<%= form_for(@listing) do |f| %>
<header>
<a href="<%= @listing.path %>" class="listings-back-button" style="font-size: 1.1em;">Last Published/Bumped: <%= time_ago_in_words @listing.bumped_at %> ago</a>
<h2>You can bump your listing for the same price as the original listing</h2>
</header>
<div class="listing-form-inner">
<% if (@listing.natural_expiration_date) < Date.today %>
<h4>
(Expired <%= time_ago_in_words @listing.natural_expiration_date %> ago)
</h4>
<% else %>
<h4>
(Expires in <%= time_ago_in_words @listing.natural_expiration_date %>)
</h4>
<% end %>
<% if @listing.bumped_at %>
<%= form_for(@listing, html: { class: "crayons-card grid gap-6 p-6 mb-6" }) do |f| %>
<header>
<h2 class="mb-2">Bump your listing</h2>
<p>You can bump your listing for the same price as the original listing</p>
</header>
<div class="flex items-center">
<%= f.submit "Bump listing", class: "crayons-btn crayons-btn--secondary mr-4" %>
<p class="color-base-70">Last Published/Bumped: <%= time_ago_in_words @listing.bumped_at %> ago</p>
</div>
<input type="hidden" name="listing[action]" value="bump" />
<%= f.submit "Bump Listing", class: "cta cta-main-listing-form" %>
</div>
<% end %>
<% end %>
<%= form_for(@listing) do |f| %>
<div class="listing-form-inner">
<% unless @listing.published %>
<h2 style="text-align: center; margin-top: 15px;">
This listing is not published
</h2>
<% end %>
<% if @listing.published == false %>
<% end %>
</div>
<aside class="crayons-layout__sidebar-right px-3 m:px-0">
<% unless @listing.published %>
<%= form_for(@listing, html: { class: "crayons-card crayons-card--secondary p-3 m:p-4 mb-2 m:mb-4" }) do |f| %>
<p class="pb-4">
This listing is <strong class="color-accent-danger">not</strong> published
</p>
<input type="hidden" name="listing[action]" value="publish" />
<%= f.submit "Publish Listing", class: "cta listings-publish cta-main-listing-form" %>
<% else %>
<input type="hidden" name="listing[action]" value="unpublish" />
<%= f.submit "Unpublish Listing", class: "cta listings-unpublish cta-main-listing-form" %>
<%= f.submit "Publish listing", class: "crayons-btn w-100" %>
<% end %>
<% end %>
<div class="crayons-card crayons-card--secondary p-3 m:p-4 mb-2 m:mb-4">
<p>
<% if (@listing.natural_expiration_date) < Date.today %>
Expired <%= time_ago_in_words @listing.natural_expiration_date %> ago
<% else %>
Expires in <%= time_ago_in_words @listing.natural_expiration_date %>
<% end %>
</p>
<% if @listing.bumped_at %>
<p class="pt-3 mt-3 border-t-1 border-solid border-0 border-base-10">
Last Published/Bumped: <%= time_ago_in_words @listing.bumped_at %> ago
</p>
<% end %>
</div>
<% end %>
</div>
</aside>
</main>

View file

@ -27,7 +27,7 @@
<% end %>
<header class="crayons-layout flex items-center justify-between p-2 l:p-4 l:pb-0">
<h1 class="">Listings</h1>
<h1 class="fs-2xl s:fs-3xl">Listings</h1>
<div class="flex items-center">
<a href="<%= new_listing_path %>" class="crayons-btn crayons-btn--secondary">Create</a>

View file

@ -1,5 +1,44 @@
<% title "New Listing" %>
<div class="listings-container listings-form-container" id="listings-container">
<%= render "form", listing: @listing %>
</div>
<header class="crayons-layout crayons-layout--limited flex items-center justify-between p-2 l:p-4 l:pb-0">
<h1 class="fs-2xl s:fs-3xl">Create a Community Listing</h1>
</header>
<main class="crayons-layout crayons-layout--2-cols crayons-layout--2-cols--inverted crayons-layout--limited">
<div class="crayons-layout__content">
<%= render "form", listing: @listing %>
</div>
<aside class="crayons-layout__sidebar-right px-3 m:px-0">
<div class="crayons-card crayons-card--secondary p-3 m:p-6 mb-2 m:mb-4">
<span class="color-base-60">Personal credits</span>
<div class="flex m:flex-col items-center m:items-start justify-between">
<strong class="fs-2xl m:fs-3xl lh-tight color-base-90 m:mb-4 block" id="test-credits-size"><%= @credits.size %></strong>
<a target="_blank" rel="noopener noreferrer" class="crayons-btn crayons-btn--secondary m:w-100" href="/credits/purchase" data-no-instant>Buy more</a>
</div>
</div>
<% if @organizations.present? %>
<div class="crayons-card crayons-card--secondary p-3 m:p-6 mb-4">
<span class="color-base-60">Organization credits</label>
<% if @organizations.size > 1 %>
<select id="org-credits-select" class="org-credits-select crayons-select my-2">
<% @organizations.each do |org| %>
<option value="<%= org.id %>" data-credits="<%= org.credits.unspent.size %>"><%= org.name %></option>
<% end %>
</select>
<% end %>
<div class="flex m:flex-col items-center m:items-start justify-between">
<strong class="fs-2xl m:fs-3xl lh-tight color-base-90 m:mb-4 block" id="org-credits-number"><%= @organizations.first.credits.unspent.size %></strong>
<a id="org-credits-purchase-link" target="_blank" rel="noopener noreferrer" class="crayons-btn crayons-btn--secondary m:w-100" href="/credits/purchase?organization_id=<%= @organizations.first.id %>" data-no-instant>Buy more</a>
</div>
<%= javascript_packs_with_chunks_tag "listingForm", "orgCreditsSelector", defer: true %>
</div>
<% end %>
<div class="fs-s color-base-60 mb-4">
<h3 class="fs-base fw-bold color-base-70 mb-2">Disclaimer</h3>
<p class="mb-2"><%= community_name %> admins may modify tags and categories to ensure accuracy. We may also take down and refund inappropriate listings.</p>
<p>Listings expire 30 days after they are published or "bumped".</p>
</div>
</aside>
</main>

View file

@ -91,7 +91,6 @@ RSpec.describe "/listings", type: :request do
context "when the user has no credits" do
it "shows the proper messages" do
get "/listings/new"
expect(response.body).to include "Listings Require Credits"
expect(response.body).to include "You need at least one credit to create a listing."
end
end
@ -101,7 +100,8 @@ RSpec.describe "/listings", type: :request do
random_number = rand(2..100)
create_list(:credit, random_number, user: user)
get "/listings/new"
expect(response.body).to include "You have #{random_number} credits available"
expect(response.body).to include "Personal credits"
expect(response.body).to include random_number.to_s
end
end
@ -112,21 +112,23 @@ RSpec.describe "/listings", type: :request do
it "shows the proper message when both user and org have no credits" do
get "/listings/new"
expect(response.body).to include "Listings Require Credits"
expect(response.body).to include "You need at least one credit to create a listing."
end
it "shows the number of credits of the user if the user has credits but the org has no credits" do
random_number = rand(2..100)
create_list(:credit, random_number, user: user)
get "/listings/new"
expect(response.body).to include "You have #{random_number} credits available"
expect(response.body).to include "Personal credits"
expect(response.body).to include random_number.to_s
end
it "shows the number of credits of the organization if the org has credits" do
random_number = rand(2..100)
create_list(:credit, random_number, organization: organization)
get "/listings/new"
expect(response.body).to include "has <span id=\"org-credits-number\">#{random_number}</span> credits"
expect(response.body).to include "Organization credits"
expect(response.body).to include random_number.to_s
end
it "shows the number of credits of both the user and the organization if they both have credits" do
@ -134,8 +136,8 @@ RSpec.describe "/listings", type: :request do
create_list(:credit, random_number, organization: organization)
create_list(:credit, random_number, user: user)
get "/listings/new"
expect(response.body).to include "has <span id=\"org-credits-number\">#{random_number}</span> credits"
expect(response.body).to include "You have #{random_number} credits available"
expect(response.body).to include "Personal credits"
expect(response.body).to include random_number.to_s
end
end
end