Move new listing form to Preact (#2674)
* Fix typo * Use preact for new listing form * Send proper max tag amount in error msg * Add styles back in for edit page
This commit is contained in:
parent
d8c38bd855
commit
07796e769a
12 changed files with 589 additions and 31 deletions
|
|
@ -186,7 +186,7 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.listings-back-buton {
|
||||
.listings-back-button {
|
||||
font-size: 0.8em;
|
||||
margin: 5px 0px 10px;
|
||||
display: block;
|
||||
|
|
@ -263,4 +263,52 @@
|
|||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.listingform__input {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.listingform__bodymarkdown {
|
||||
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;
|
||||
}
|
||||
|
||||
.listingform__tagoptionrow {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: lighten($green, 27%);
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
1
app/assets/stylesheets/classified_listings_form.scss
Normal file
1
app/assets/stylesheets/classified_listings_form.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import 'variables';
|
||||
26
app/javascript/listings/elements/bodyMarkdown.jsx
Normal file
26
app/javascript/listings/elements/bodyMarkdown.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const BodyMarkdown = ({ onChange, defaultValue }) => (
|
||||
<div className="field">
|
||||
<label className="listingform__label" htmlFor="body_markdown">
|
||||
Body Markdown
|
||||
<textarea
|
||||
className="listingform__input listingform__bodymarkdown"
|
||||
id="body_markdown"
|
||||
name="classified_listing[body_markdown]"
|
||||
maxLength="400"
|
||||
placeholder="400 characters max, 12 line break max, no images allowed"
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
|
||||
BodyMarkdown.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default BodyMarkdown;
|
||||
56
app/javascript/listings/elements/categories.jsx
Normal file
56
app/javascript/listings/elements/categories.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class Categories extends Component {
|
||||
options = () => {
|
||||
const { categoriesForSelect } = this.props
|
||||
return categoriesForSelect.map(array => {
|
||||
return(
|
||||
<option value={array[1]}>{array[0]}</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
details = () => {
|
||||
const { categoriesForDetails } = this.props
|
||||
const rules = categoriesForDetails.map(category => {
|
||||
const paragraphText = `${category.name}: ${category.rules}`
|
||||
return(
|
||||
<p>
|
||||
{paragraphText}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
|
||||
return(
|
||||
<details>
|
||||
<summary>
|
||||
Category details/rules
|
||||
</summary>
|
||||
{rules}
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div className="field">
|
||||
<label className="listingform__label" htmlFor="category">
|
||||
Category
|
||||
</label>
|
||||
<select className="listingform__input" name="classified_listing[category]">
|
||||
{this.options()}
|
||||
</select>
|
||||
{this.details()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Categories.propTypes = {
|
||||
categoriesForSelect: PropTypes.array.isRequired,
|
||||
categoriesForDetails: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
|
||||
export default Categories;
|
||||
0
app/javascript/listings/elements/organizationField.jsx
Normal file
0
app/javascript/listings/elements/organizationField.jsx
Normal file
335
app/javascript/listings/elements/tags.jsx
Normal file
335
app/javascript/listings/elements/tags.jsx
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const KEYS = {
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
TAB: 9,
|
||||
RETURN: 13,
|
||||
COMMA: 188,
|
||||
DELETE: 8,
|
||||
};
|
||||
|
||||
const MAX_TAGS = 8;
|
||||
|
||||
class Tags extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedIndex: -1,
|
||||
searchResults: [],
|
||||
cursorIdx: 0,
|
||||
prevLen: 0,
|
||||
};
|
||||
|
||||
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);
|
||||
this.index = client.initIndex(`Tag_${env}`);
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this.props.defaultValue
|
||||
.split(',')
|
||||
.map(item => item !== undefined && item.trim())
|
||||
.filter(item => item.length > 0);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// stop cursor jumping if the user goes back to edit previous tags
|
||||
if (
|
||||
this.state.cursorIdx < this.textArea.value.length &&
|
||||
this.textArea.value.length < this.state.prevLen + 1
|
||||
) {
|
||||
this.textArea.selectionStart = this.textArea.selectionEnd = this.state.cursorIdx;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let searchResultsHTML = '';
|
||||
const searchResultsRows = this.state.searchResults.map((tag, index) => (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
className={`listingform__tagoptionrow listingform__tagoptionrow--${
|
||||
this.state.selectedIndex === index ? 'active' : 'inactive'
|
||||
}`}
|
||||
onClick={this.handleTagClick}
|
||||
data-content={tag.name}
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
));
|
||||
if (
|
||||
this.state.searchResults.length > 0 &&
|
||||
document.activeElement.id === 'tag-input'
|
||||
) {
|
||||
searchResultsHTML = (
|
||||
<div className="listingform__tagsoptions">{searchResultsRows}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="listingform__tagswrapper">
|
||||
<label htmlFor="Tags">Tags</label>
|
||||
<input
|
||||
id="tag-input"
|
||||
name="classified_listing[tag_list]"
|
||||
type="text"
|
||||
ref={t => (this.textArea = t)}
|
||||
className="listingform__input listingform__tagsinput"
|
||||
placeholder="8 tags max, comma separated, no spaces or special characters"
|
||||
value={this.props.defaultValue}
|
||||
onInput={this.handleInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleFocusChange}
|
||||
onFocus={this.handleFocusChange}
|
||||
/>
|
||||
{searchResultsHTML}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleTagClick = e => {
|
||||
const input = document.getElementById('tag-input');
|
||||
input.focus();
|
||||
|
||||
this.insertTag(e.target.dataset.content);
|
||||
};
|
||||
|
||||
handleInput = e => {
|
||||
let { value } = e.target;
|
||||
// If we start typing immediately after a comma, add a space
|
||||
// before what we typed.
|
||||
// e.g. If value = "javascript," and we type a "p",
|
||||
// the result should be "javascript, p".
|
||||
if (e.inputType === 'insertText') {
|
||||
const isTypingAfterComma =
|
||||
e.target.value[e.target.selectionStart - 2] === ',';
|
||||
if (isTypingAfterComma) {
|
||||
value = this.insertSpace(value, e.target.selectionStart - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.data === ',') {
|
||||
value += ' ';
|
||||
}
|
||||
|
||||
this.props.onInput(value);
|
||||
|
||||
const query = this.getCurrentTagAtSelectionIndex(
|
||||
e.target.value,
|
||||
e.target.selectionStart - 1,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
selectedIndex: 0,
|
||||
cursorIdx: e.target.selectionStart,
|
||||
prevLen: this.textArea.value.length,
|
||||
});
|
||||
|
||||
return this.search(query);
|
||||
};
|
||||
|
||||
insertSpace(value, position) {
|
||||
return `${value.slice(0, position)} ${value.slice(position, value.length)}`;
|
||||
}
|
||||
|
||||
getCurrentTagAtSelectionIndex(value, index) {
|
||||
let tagIndex = 0;
|
||||
const tagByCharacterIndex = {};
|
||||
|
||||
value.split('').map((letter, index) => {
|
||||
if (letter === ',') {
|
||||
tagIndex++;
|
||||
} else {
|
||||
tagByCharacterIndex[index] = tagIndex;
|
||||
}
|
||||
});
|
||||
|
||||
const tag = value.split(',')[tagByCharacterIndex[index]];
|
||||
|
||||
if (tag === undefined) {
|
||||
return '';
|
||||
}
|
||||
return tag.trim();
|
||||
}
|
||||
|
||||
search(query) {
|
||||
if (query === '') {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
this.resetSearchResults();
|
||||
resolve();
|
||||
}, 5);
|
||||
});
|
||||
}
|
||||
|
||||
return this.index
|
||||
.search(query, {
|
||||
hitsPerPage: 10,
|
||||
attributesToHighlight: [],
|
||||
filters: 'supported:true',
|
||||
})
|
||||
.then(content => {
|
||||
this.setState({
|
||||
searchResults: content.hits.filter(
|
||||
hit => !this.selected.includes(hit.name),
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetSearchResults() {
|
||||
this.setState({
|
||||
searchResults: [],
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const component = this;
|
||||
|
||||
if (component.selected.length === MAX_TAGS && e.keyCode === KEYS.COMMA) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(e.keyCode === KEYS.DOWN || e.keyCode === KEYS.TAB) &&
|
||||
!this.isBottomOfSearchResults &&
|
||||
component.props.defaultValue != ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.moveDownInSearchResults();
|
||||
} else if (e.keyCode === KEYS.UP && !this.isTopOfSearchResults) {
|
||||
e.preventDefault();
|
||||
this.moveUpInSearchResults();
|
||||
} else if (e.keyCode === KEYS.RETURN && this.isSearchResultSelected) {
|
||||
e.preventDefault();
|
||||
this.insertTag(
|
||||
component.state.searchResults[component.state.selectedIndex].name,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('tag-input').focus();
|
||||
}, 10);
|
||||
} else if (e.keyCode === KEYS.COMMA && !this.isSearchResultSelected) {
|
||||
this.resetSearchResults();
|
||||
this.clearSelectedSearchResult();
|
||||
} else if (e.keyCode === KEYS.DELETE) {
|
||||
if (
|
||||
component.props.defaultValue[
|
||||
component.props.defaultValue.length - 1
|
||||
] === ','
|
||||
) {
|
||||
this.clearSelectedSearchResult();
|
||||
}
|
||||
} else if (
|
||||
(e.keyCode < 65 || e.keyCode > 90) &&
|
||||
e.keyCode != KEYS.COMMA &&
|
||||
e.keyCode != KEYS.DELETE &&
|
||||
e.keyCode != KEYS.LEFT &&
|
||||
e.keyCode != KEYS.RIGHT &&
|
||||
e.keyCode != KEYS.TAB
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
moveUpInSearchResults() {
|
||||
this.setState({
|
||||
selectedIndex: this.state.selectedIndex - 1,
|
||||
});
|
||||
}
|
||||
|
||||
moveDownInSearchResults() {
|
||||
this.setState({
|
||||
selectedIndex: this.state.selectedIndex + 1,
|
||||
});
|
||||
}
|
||||
|
||||
get isTopOfSearchResults() {
|
||||
return this.state.selectedIndex <= 0;
|
||||
}
|
||||
|
||||
get isBottomOfSearchResults() {
|
||||
return this.state.selectedIndex >= this.state.searchResults.length - 1;
|
||||
}
|
||||
|
||||
get isSearchResultSelected() {
|
||||
return this.state.selectedIndex > -1;
|
||||
}
|
||||
|
||||
clearSelectedSearchResult() {
|
||||
this.setState({
|
||||
selectedIndex: -1,
|
||||
});
|
||||
}
|
||||
|
||||
insertTag(tag) {
|
||||
const input = document.getElementById('tag-input');
|
||||
|
||||
const range = this.getRangeBetweenCommas(input.value, input.selectionStart);
|
||||
const insertingAtEnd = range[1] === input.value.length;
|
||||
const maxTagsWillBeReached = this.selected.length === MAX_TAGS;
|
||||
if (insertingAtEnd && !maxTagsWillBeReached) {
|
||||
tag += ', ';
|
||||
}
|
||||
|
||||
// Insert new tag between commas if there are any.
|
||||
const newInput =
|
||||
input.value.slice(0, range[0]) +
|
||||
tag +
|
||||
input.value.slice(range[1], input.value.length);
|
||||
|
||||
this.props.onInput(newInput);
|
||||
this.resetSearchResults();
|
||||
this.clearSelectedSearchResult();
|
||||
}
|
||||
|
||||
// Given an index of the String value, finds the range between commas.
|
||||
// This is useful when we want to insert a new tag anywhere in the
|
||||
// comma separated list of tags.
|
||||
getRangeBetweenCommas(value, index) {
|
||||
let start = 0;
|
||||
let end = value.length;
|
||||
|
||||
const toPreviousComma = value
|
||||
.slice(0, index)
|
||||
.split('')
|
||||
.reverse()
|
||||
.indexOf(',');
|
||||
const toNextComma = value.slice(index).indexOf(',');
|
||||
|
||||
if (toPreviousComma !== -1) {
|
||||
start = index - toPreviousComma + 1;
|
||||
}
|
||||
|
||||
if (toNextComma !== -1) {
|
||||
end = index + toNextComma;
|
||||
}
|
||||
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
handleFocusChange = e => {
|
||||
const component = this;
|
||||
setTimeout(() => {
|
||||
if (document.activeElement.id === 'tag-input') {
|
||||
return;
|
||||
}
|
||||
component.forceUpdate();
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
|
||||
Tags.propTypes = {
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
32
app/javascript/listings/elements/title.jsx
Normal file
32
app/javascript/listings/elements/title.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
const domId = 1;
|
||||
|
||||
const Title = ( { onChange, defaultValue }) => (
|
||||
<div className="field">
|
||||
<label className="listingform__label" htmlFor={domId}>
|
||||
Title
|
||||
<input
|
||||
type="text"
|
||||
className="listingform__input"
|
||||
id={domId}
|
||||
name="classified_listing[title]"
|
||||
maxLength="128"
|
||||
size="128"
|
||||
placeholder="128 characters max, plain text"
|
||||
autoComplete="off"
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
|
||||
Title.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
|
||||
export default Title;
|
||||
63
app/javascript/listings/listingForm.jsx
Normal file
63
app/javascript/listings/listingForm.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { h, Component } from 'preact';
|
||||
import linkState from 'linkstate';
|
||||
import Title from './elements/title';
|
||||
import BodyMarkdown from './elements/bodyMarkdown';
|
||||
import Categories from './elements/categories';
|
||||
import Tags from './elements/tags';
|
||||
|
||||
export default class ListingForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.listing = JSON.parse(this.props.listing);
|
||||
this.categoriesForDetails = JSON.parse(this.props.categoriesForDetails);
|
||||
this.categoriesForSelect = JSON.parse(this.props.categoriesForSelect);
|
||||
|
||||
const organizations = this.props.organizations
|
||||
? JSON.parse(this.props.organizations) : null;
|
||||
|
||||
this.url = window.location.href;
|
||||
|
||||
this.state = {
|
||||
id: this.listing.id || null,
|
||||
title: this.listing.title || '',
|
||||
tagList: this.listing.cached_tag_list || '',
|
||||
bodyMarkdown: this.listing.body_markdown || '',
|
||||
organizations: organizations || null,
|
||||
categoriesForSelect: this.categoriesForSelect,
|
||||
categoriesForDetails: this.categoriesForDetails,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
bodyMarkdown,
|
||||
tagList,
|
||||
organizations,
|
||||
categoriesForDetails,
|
||||
categoriesForSelect,
|
||||
} = this.state;
|
||||
|
||||
if (id === null) {
|
||||
return(
|
||||
<div>
|
||||
<Title defaultValue={title} onChange={linkState(this, 'title')} />
|
||||
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
|
||||
<Categories categoriesForSelect={categoriesForSelect} categoriesForDetails={categoriesForDetails} />
|
||||
<Tags defaultValue={tagList} onInput={linkState(this, 'tagList')} />
|
||||
{/* add contact via connect checkbox later */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return(
|
||||
<div>
|
||||
<Title defaultValue={title} onChange={linkState(this, 'title')} />
|
||||
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
|
||||
<Tags defaultValue={tagList} onInput={linkState(this, 'tagList')} />
|
||||
{/* add contact via connect checkbox later */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
15
app/javascript/packs/listingForm.jsx
Normal file
15
app/javascript/packs/listingForm.jsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { h, render } from 'preact';
|
||||
import ListingForm from '../listings/listingForm';
|
||||
|
||||
const root = document.getElementById('listingform-data');
|
||||
const { listing, organizations, categoriesForSelect, categoriesForDetails } = root.dataset;
|
||||
|
||||
render(
|
||||
<ListingForm
|
||||
organizations={organizations}
|
||||
listing={listing}
|
||||
categoriesForSelect={categoriesForSelect}
|
||||
categoriesForDetails={categoriesForDetails}
|
||||
/>,
|
||||
root,
|
||||
);
|
||||
|
|
@ -66,7 +66,7 @@ class ClassifiedListing < ApplicationRecord
|
|||
"collabs" => { cost: 1, name: "Contributors/Collaborators Wanted" },
|
||||
"education" => { cost: 1, name: "Education/Courses", rules: "Educational material and/or schools/bootcamps" },
|
||||
"jobs" => { cost: 10, name: "Job Listings", rules: "Companies offering employment right now." },
|
||||
"products" => { cost: 1, name: "Products/Tools", rules: "Must be availabel right now" },
|
||||
"products" => { cost: 1, name: "Products/Tools", rules: "Must be available right now" },
|
||||
"events" => { cost: 1, name: "Upcoming Events", rules: "Live or online events with date included" },
|
||||
"misc" => { cost: 1, name: "Miscellaneous", rules: "Must not fit in any other category." }
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ class ClassifiedListing < ApplicationRecord
|
|||
end
|
||||
|
||||
def validate_tags
|
||||
errors.add(:tag_list, "exceed the maximum of 4 tags") if tag_list.length > 8
|
||||
errors.add(:tag_list, "exceed the maximum of 8 tags") if tag_list.length > 8
|
||||
end
|
||||
|
||||
def validate_category
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<%= form_with(model: classified_listing, local: true) do |form| %>
|
||||
<a href="/listings" class="listings-back-buton">< go back to listings</a>
|
||||
<a href="/listings" class="listings-back-button">< go back to listings</a>
|
||||
<% if classified_listing.errors.any? %>
|
||||
<div class="classified-errors">
|
||||
<h2><%= pluralize(classified_listing.errors.count, "error") %> prohibited this classified_listing from being saved:</h2>
|
||||
|
|
@ -11,32 +11,14 @@
|
|||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="field">
|
||||
<%= form.label "title", "Title" %>
|
||||
<%= form.text_field "title", maxlength: 128, placeholder: "128 characters max, plain text" %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= form.label "body_markdown", "Body Markdown" %>
|
||||
<%= form.text_area "body_markdown", maxlength: 400, placeholder: "400 characters max, 12 line break max, no images allowed" %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= form.label "category" %>
|
||||
<%= form.select :category, options_for_select(ClassifiedListing.select_options_for_categories, ClassifiedListing.select_options_for_categories.first) %>
|
||||
<details>
|
||||
<summary>
|
||||
Category details/rules
|
||||
</summary>
|
||||
<% ClassifiedListing.categories_available.keys.each do |key| %>
|
||||
<p>
|
||||
<%= ClassifiedListing.categories_available[key][:name] %>: <%= ClassifiedListing.categories_available[key][:rules] %>
|
||||
</p>
|
||||
<% end %>
|
||||
</details>
|
||||
</div>
|
||||
<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 id="listingform-data"
|
||||
data-listing="<%= classified_listing.to_json(only: %i[id title slug cached_tag_list]) %>"
|
||||
data-organizations="<%= @organizations.to_json %>"
|
||||
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>
|
||||
<%= javascript_pack_tag "listingForm", defer: true %>
|
||||
<% if false # Put connect button (future feature) %>
|
||||
<div class="field">
|
||||
<%= form.label "contact_via_connect" %>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="classifieds-container">
|
||||
<div class="classifieds-container" id="classifieds-container">
|
||||
<%= render 'form', classified_listing: @classified_listing %>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue