Add dropdown menu to listings (#4184) [deploy]
* Resolves 2826, add dropdown menu to listings * Fixed dropdown position * Refactored single listing * Revert schema.rb to master * Fixed tests
This commit is contained in:
parent
35c2800c89
commit
cf2af6c2dc
12 changed files with 445 additions and 285 deletions
|
|
@ -62,11 +62,7 @@
|
|||
position: relative;
|
||||
input {
|
||||
font-size: 0.8em;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
|
|
@ -105,7 +101,9 @@
|
|||
}
|
||||
&.clear-all {
|
||||
background: $light-medium-gray;
|
||||
a { color: $black }
|
||||
a {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: white;
|
||||
|
|
@ -121,24 +119,16 @@
|
|||
.classifieds-columns {
|
||||
display: grid;
|
||||
grid-gap: 1em;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px,1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-auto-rows: 0;
|
||||
}
|
||||
form {
|
||||
margin: 60px auto 0px;
|
||||
width: 100%;
|
||||
max-width: 580px;
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background,
|
||||
white
|
||||
);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
box-sizing: border-box;
|
||||
@include themeable(
|
||||
border,
|
||||
theme-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
@include themeable(border, theme-border, 1px solid $light-medium-gray);
|
||||
@include themeable(
|
||||
box-shadow,
|
||||
theme-container-box-shadow,
|
||||
|
|
@ -155,12 +145,11 @@
|
|||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
#f2f7fc
|
||||
#f2f7fc
|
||||
);
|
||||
padding: 25px 22px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
|
||||
}
|
||||
.classified-form-inner {
|
||||
padding: 0px 15px 40px;
|
||||
|
|
@ -175,11 +164,7 @@
|
|||
}
|
||||
h2 {
|
||||
font-size: 1.45em;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
#32325d
|
||||
);
|
||||
@include themeable(color, theme-color, #32325d);
|
||||
margin: 0px;
|
||||
}
|
||||
.classified-errors {
|
||||
|
|
@ -210,12 +195,8 @@
|
|||
theme-container-accent-background,
|
||||
$light-gray
|
||||
);
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
&[type="checkbox"] {
|
||||
@include themeable(color, theme-color, $black);
|
||||
&[type='checkbox'] {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
|
@ -230,33 +211,30 @@
|
|||
theme-container-accent-background,
|
||||
$light-gray
|
||||
);
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
border: 1px solid $light-medium-gray;
|
||||
}
|
||||
select {
|
||||
width: 100%;
|
||||
font-size:20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
details {
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
padding: 8px 0px;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-secondary-color,
|
||||
$medium-gray
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
}
|
||||
.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%);
|
||||
background-image: linear-gradient(
|
||||
141deg,
|
||||
$dark-purple 0%,
|
||||
#1f89db 51%,
|
||||
#2981e5 75%
|
||||
);
|
||||
border-radius: 100px;
|
||||
font-size: 1.4em;
|
||||
color: white;
|
||||
|
|
@ -283,7 +261,12 @@
|
|||
right: 0px;
|
||||
padding: 18px 0px;
|
||||
background-color: #aecfef;
|
||||
background-image: linear-gradient(141deg, #aecfef 0%, #ddeeff 51%, #c8ebf1 75%);
|
||||
background-image: linear-gradient(
|
||||
141deg,
|
||||
#aecfef 0%,
|
||||
#ddeeff 51%,
|
||||
#c8ebf1 75%
|
||||
);
|
||||
color: #32325d;
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
|
|
@ -298,11 +281,7 @@
|
|||
a {
|
||||
text-decoration: underline;
|
||||
display: inline-block;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-anchor-color,
|
||||
$bold-blue
|
||||
);
|
||||
@include themeable(color, theme-anchor-color, $bold-blue);
|
||||
}
|
||||
@media screen and (min-width: 580px) {
|
||||
font-size: 1.1em;
|
||||
|
|
@ -313,27 +292,15 @@
|
|||
}
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-secondary-color,
|
||||
$medium-gray
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
}
|
||||
}
|
||||
.classifieds-load-more-button {
|
||||
text-align: center;
|
||||
button {
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
background: transparent;
|
||||
@include themeable(
|
||||
border,
|
||||
theme-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
@include themeable(border, theme-border, 1px solid $light-medium-gray);
|
||||
font-size: 17px;
|
||||
padding: 14px 5px;
|
||||
margin: 40px auto 70px;
|
||||
|
|
@ -354,26 +321,14 @@
|
|||
padding: 5px;
|
||||
font-size: 17px;
|
||||
border-radius: 3px;
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background,
|
||||
white
|
||||
);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
margin-top: 10px;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
border: 1px solid $light-medium-gray;
|
||||
}
|
||||
|
||||
.listingform__tagsoptions {
|
||||
@include themeable(
|
||||
border,
|
||||
theme-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
@include themeable(border, theme-border, 1px solid $light-medium-gray);
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
|
@ -388,7 +343,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.listingform__tagsoptionsbottomrow, .listingform__tagsoptionrulesbutton, .listingform__tagrules--inactive {
|
||||
.listingform__tagsoptionsbottomrow,
|
||||
.listingform__tagsoptionrulesbutton,
|
||||
.listingform__tagrules--inactive {
|
||||
display: none; //not yet fully functional or relevant
|
||||
}
|
||||
|
||||
|
|
@ -457,14 +414,13 @@
|
|||
text-overflow: ellipsis;
|
||||
font-size: 0.9em;
|
||||
break-inside: avoid;
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background,
|
||||
white
|
||||
);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
h3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0px;
|
||||
padding: 9px 15px;
|
||||
position: relative;
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
|
|
@ -472,7 +428,56 @@
|
|||
);
|
||||
border-bottom: 1px solid $light-medium-gray;
|
||||
font-size: 16px;
|
||||
a { @include themeable(color, theme-color, $black); }
|
||||
a {
|
||||
@include themeable(color, theme-color, $black);
|
||||
}
|
||||
}
|
||||
.dropdown-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
.dropdown-icon {
|
||||
max-width: 15px;
|
||||
max-height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: inline-block;
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border: 1px solid $light-medium-gray;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
z-index: 20;
|
||||
width: 130px;
|
||||
box-shadow: $shadow;
|
||||
&.showing {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
width: 98px;
|
||||
height: 14px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
.single-classified-listing-body {
|
||||
padding: 10px 15px;
|
||||
|
|
@ -491,11 +496,7 @@
|
|||
.single-classified-listing-tags {
|
||||
padding: 10px 15px;
|
||||
a {
|
||||
@include themeable(
|
||||
color,
|
||||
theme-secondary-color,
|
||||
$dark-medium-gray
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, $dark-medium-gray);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
|
|
@ -511,18 +512,10 @@
|
|||
}
|
||||
.single-classified-listing-author-info {
|
||||
font-size: 0.69em;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-secondary-color,
|
||||
$medium-gray
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
padding: 0px 15px 4px;
|
||||
a {
|
||||
@include themeable(
|
||||
color,
|
||||
theme-secondary-color,
|
||||
$medium-gray
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
display: inline-block;
|
||||
&.classified-listing-edit-button {
|
||||
margin-left: 4px;
|
||||
|
|
@ -537,7 +530,9 @@
|
|||
right: 0px;
|
||||
padding-bottom: 12px;
|
||||
@media screen and (min-width: 950px) {
|
||||
h3 { font-size: calc(20px + 0.05vw);}
|
||||
h3 {
|
||||
font-size: calc(20px + 0.05vw);
|
||||
}
|
||||
.single-classified-listing-body {
|
||||
font-size: calc(18px + 0.05vw);
|
||||
}
|
||||
|
|
@ -554,17 +549,9 @@
|
|||
border-radius: 100px;
|
||||
font-weight: bold;
|
||||
margin: 40px auto 10px;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
border: 3px solid white;
|
||||
@include themeable(
|
||||
border-color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(border-color, theme-color, $black);
|
||||
@include themeable(
|
||||
background-color,
|
||||
theme-container-accent-background,
|
||||
|
|
@ -572,7 +559,6 @@
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
form.listings-contact-via-connect {
|
||||
border-radius: 3px;
|
||||
z-index: 20;
|
||||
|
|
@ -584,7 +570,9 @@ form.listings-contact-via-connect {
|
|||
box-shadow: none;
|
||||
max-width: 100%;
|
||||
text-align: right;
|
||||
p {text-align: left}
|
||||
p {
|
||||
text-align: left;
|
||||
}
|
||||
width: 94%;
|
||||
max-width: 600px;
|
||||
textarea#new-message {
|
||||
|
|
@ -611,7 +599,10 @@ form.listings-contact-via-connect {
|
|||
.classified-listings-modal-background {
|
||||
z-index: 15;
|
||||
position: fixed;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
user-select: none;
|
||||
}
|
||||
|
|
@ -635,11 +626,7 @@ form.listings-contact-via-connect {
|
|||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
user-select: none;
|
||||
@include themeable(
|
||||
color,
|
||||
theme-color,
|
||||
$black
|
||||
);
|
||||
@include themeable(color, theme-color, $black);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background,
|
||||
|
|
@ -654,16 +641,8 @@ form.listings-contact-via-connect {
|
|||
}
|
||||
|
||||
&.active {
|
||||
@include themeable(
|
||||
background,
|
||||
theme-secondary-color,
|
||||
$dark-gray
|
||||
);
|
||||
@include themeable(
|
||||
color,
|
||||
theme-container-background,
|
||||
white
|
||||
);
|
||||
@include themeable(background, theme-secondary-color, $dark-gray);
|
||||
@include themeable(color, theme-container-background, white);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -754,7 +733,6 @@ form.listings-contact-via-connect {
|
|||
}
|
||||
|
||||
.dashboard-listings-view {
|
||||
|
||||
.dashboard-listing-row {
|
||||
border: var(--theme-container-border, 1px solid #dbdbdb);
|
||||
box-shadow: var(--theme-container-box-shadow, 1px 1px 0px #c2c2c2);
|
||||
|
|
@ -765,7 +743,7 @@ form.listings-contact-via-connect {
|
|||
background: var(--theme-container-background, #fff);
|
||||
|
||||
&.draft {
|
||||
background: var(--theme-container-accent-background, #fffeec)
|
||||
background: var(--theme-container-accent-background, #fffeec);
|
||||
}
|
||||
|
||||
&.expired {
|
||||
|
|
@ -808,11 +786,7 @@ form.listings-contact-via-connect {
|
|||
.dashboard-listing-category {
|
||||
padding: 10px 15px;
|
||||
a {
|
||||
@include themeable(
|
||||
color,
|
||||
theme-secondary-color,
|
||||
$dark-medium-gray
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, $dark-medium-gray);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
|
|
@ -868,9 +842,6 @@ form.listings-contact-via-connect {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SingeListing /> should load a single user listing 1`] = `
|
||||
exports[`<SingleListing /> should load a single user listing 1`] = `
|
||||
preact-render-spy (1 nodes)
|
||||
-------
|
||||
<div
|
||||
|
|
@ -8,7 +8,7 @@ preact-render-spy (1 nodes)
|
|||
id="single-classified-listing-22"
|
||||
>
|
||||
<div class="listing-content">
|
||||
<h3>
|
||||
<h3 class="single-classified-listing-header">
|
||||
<a
|
||||
href="/listings/misc/illo-iure-quos-perspiciatis-5hk7"
|
||||
data-no-instant={true}
|
||||
|
|
@ -17,6 +17,25 @@ preact-render-spy (1 nodes)
|
|||
>
|
||||
Illo iure quos perspiciatis.
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-btn"
|
||||
aria-label="Toggle dropdown menu"
|
||||
onClick={[Function value]}
|
||||
>
|
||||
<img
|
||||
src=""
|
||||
class="dropdown-icon"
|
||||
alt="Dropdown menu icon"
|
||||
/>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-content ">
|
||||
<a href="/report-abuse?url=https://dev.to/listings/misc/illo-iure-quos-perspiciatis-5hk7">Report Abuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="single-classified-listing-body">
|
||||
<p>Eius et ullam. Dolores et qui. Quis <strong>qui</strong> omnis.</p>
|
||||
|
|
@ -49,7 +68,6 @@ preact-render-spy (1 nodes)
|
|||
<a href="/listings/?q=West Refugio">・West Refugio</a>
|
||||
・
|
||||
<a href="/mrschristiansenyoko">Mrs. Yoko Christiansen</a>
|
||||
<a href="/report-abuse?url=https://dev.to/listings/misc/illo-iure-quos-perspiciatis-5hk7">・report abuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { h } from 'preact';
|
||||
import { deep, shallow } from 'preact-render-spy';
|
||||
import { SingleListing } from '../singleListing';
|
||||
import { deep } from 'preact-render-spy';
|
||||
import SingleListing from '../singleListing';
|
||||
|
||||
const listing = {
|
||||
id: 22,
|
||||
|
|
@ -21,7 +21,7 @@ const listing = {
|
|||
},
|
||||
};
|
||||
|
||||
describe('<SingeListing />', () => {
|
||||
describe('<SingleListing />', () => {
|
||||
it('should load a single user listing', () => {
|
||||
const tree = deep(
|
||||
<SingleListing
|
||||
|
|
@ -43,7 +43,7 @@ describe('<SingeListing />', () => {
|
|||
});
|
||||
|
||||
describe('should load the following elements', () => {
|
||||
const context = shallow(
|
||||
const context = deep(
|
||||
<SingleListing
|
||||
onAddTag={() => {
|
||||
return 'onAddTag';
|
||||
|
|
@ -57,57 +57,62 @@ describe('<SingeListing />', () => {
|
|||
return 'onOpenModal';
|
||||
}}
|
||||
isOpen={false}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
expect(context.find('.single-classified-listing').exists()).toBeTruthy();
|
||||
|
||||
it('for listing title', () => {
|
||||
expect(
|
||||
context.find('.listing-content')
|
||||
context
|
||||
.find('.single-classified-listing-header')
|
||||
.at(0)
|
||||
.childAt(0)
|
||||
.text()
|
||||
.childAt(0)
|
||||
.text(),
|
||||
).toEqual('Illo iure quos perspiciatis.');
|
||||
});
|
||||
|
||||
it('for listing tags', () => {
|
||||
expect(
|
||||
context.find('.single-classified-listing-tags')
|
||||
context
|
||||
.find('.single-classified-listing-tags')
|
||||
.childAt(0)
|
||||
.text()
|
||||
.text(),
|
||||
).toEqual(listing.tag_list[0]);
|
||||
});
|
||||
|
||||
it('for listing category', () => {
|
||||
expect(
|
||||
context.find('.single-classified-listing-author-info')
|
||||
context
|
||||
.find('.single-classified-listing-author-info')
|
||||
.childAt(0)
|
||||
.text()
|
||||
.text(),
|
||||
).toEqual(listing.category);
|
||||
});
|
||||
|
||||
it('for listing location', () => {
|
||||
expect(
|
||||
context.find('.single-classified-listing-author-info')
|
||||
context
|
||||
.find('.single-classified-listing-author-info')
|
||||
.childAt(1)
|
||||
.text()
|
||||
.text(),
|
||||
).toEqual(`・${listing.location}`);
|
||||
});
|
||||
|
||||
it('for listing author', () => {
|
||||
expect(
|
||||
context.find('.single-classified-listing-author-info')
|
||||
context
|
||||
.find('.single-classified-listing-author-info')
|
||||
.childAt(3)
|
||||
.text()
|
||||
.text(),
|
||||
).toEqual(listing.author.name);
|
||||
});
|
||||
|
||||
it('for report abuse button', () => {
|
||||
expect(
|
||||
context.find('.single-classified-listing-author-info')
|
||||
.childAt(4)
|
||||
.text()
|
||||
).toEqual('・report abuse');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// describe('<AuthorInfo />', () => {
|
||||
// it('should load the author info of a single listing', () => {
|
||||
// const tree = deep(<AuthorInfo listing={listing} />);
|
||||
// expect(tree).toMatchSnapshot();
|
||||
// });
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { h, Component } from 'preact';
|
||||
import { SingleListing } from './singleListing';
|
||||
import SingleListing from './singleListing';
|
||||
|
||||
function resizeMasonryItem(item) {
|
||||
/* Get the grid object, its row-gap, and the size of its implicit rows */
|
||||
|
|
@ -439,7 +439,7 @@ export class Listings extends Component {
|
|||
{' '}
|
||||
<b>must</b>
|
||||
{' '}
|
||||
abide by the
|
||||
abide by the
|
||||
{' '}
|
||||
<a href="/code-of-conduct">code of conduct</a>
|
||||
</em>
|
||||
|
|
@ -449,7 +449,7 @@ abide by the
|
|||
{' '}
|
||||
<b>must</b>
|
||||
{' '}
|
||||
abide by the
|
||||
abide by the
|
||||
{' '}
|
||||
<a href="/code-of-conduct">code of conduct</a>
|
||||
</em>
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
export const SingleListing = ({
|
||||
listing,
|
||||
onAddTag,
|
||||
currentUserId,
|
||||
onChangeCategory,
|
||||
onOpenModal,
|
||||
isOpen,
|
||||
}) => {
|
||||
const tagLinks = listing.tag_list.map(tag => (
|
||||
<a
|
||||
href={`/listings?t=${tag}`}
|
||||
onClick={e => onAddTag(e, tag)}
|
||||
data-no-instant
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
));
|
||||
|
||||
const editButton =
|
||||
currentUserId === listing.user_id ? (
|
||||
<a
|
||||
href={`/listings/${listing.id}/edit`}
|
||||
className="classified-listing-edit-button"
|
||||
>
|
||||
・edit
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
href={`/report-abuse?url=https://dev.to/listings/${listing.category}/${listing.slug}`}
|
||||
>
|
||||
・report abuse
|
||||
</a>
|
||||
);
|
||||
|
||||
const locationText = listing.location ? (
|
||||
<a href={`/listings/?q=${listing.location}`}>
|
||||
・
|
||||
{listing.location}
|
||||
</a>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
const definedClass = isOpen
|
||||
? 'single-classified-listing single-classified-listing--opened'
|
||||
: 'single-classified-listing';
|
||||
|
||||
const listingCard = () => {
|
||||
return (
|
||||
<div
|
||||
className={definedClass}
|
||||
id={`single-classified-listing-${listing.id}`}
|
||||
>
|
||||
<div className="listing-content">
|
||||
<h3>
|
||||
<a
|
||||
href={`/listings/${listing.category}/${listing.slug}`}
|
||||
data-no-instant
|
||||
onClick={e => onOpenModal(e, listing)}
|
||||
data-listing-id={listing.id}
|
||||
>
|
||||
{listing.title}
|
||||
</a>
|
||||
</h3>
|
||||
<div
|
||||
className="single-classified-listing-body"
|
||||
dangerouslySetInnerHTML={{ __html: listing.processed_html }}
|
||||
/>
|
||||
<div className="single-classified-listing-tags">{tagLinks}</div>
|
||||
<div className="single-classified-listing-author-info">
|
||||
<a
|
||||
href={`/listings/${listing.category}`}
|
||||
onClick={e => onChangeCategory(e, listing.category)}
|
||||
data-no-instant
|
||||
>
|
||||
{listing.category}
|
||||
</a>
|
||||
{locationText}
|
||||
・
|
||||
<a href={`/${listing.author.username}`}>{listing.author.name}</a>
|
||||
{editButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return listingCard();
|
||||
};
|
||||
|
||||
SingleListing.propTypes = {
|
||||
listing: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
category: PropTypes.string,
|
||||
contact_via_connect: PropTypes.bool,
|
||||
location: PropTypes.string,
|
||||
processed_html: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
user_id: PropTypes.number,
|
||||
tag_list: PropTypes.arrayOf(PropTypes.string),
|
||||
author: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
onAddTag: PropTypes.func.isRequired,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
onChangeCategory: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.number,
|
||||
};
|
||||
52
app/javascript/listings/singleListing/AuthorInfo.jsx
Normal file
52
app/javascript/listings/singleListing/AuthorInfo.jsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
|
||||
const LocationText = ({ location }) => {
|
||||
return location ? (
|
||||
<a href={`/listings/?q=${location}`}>
|
||||
{'・'}
|
||||
{location}
|
||||
</a>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
LocationText.propTypes = {
|
||||
location: PropTypes.string,
|
||||
};
|
||||
|
||||
LocationText.defaultProps = {
|
||||
location: null,
|
||||
};
|
||||
|
||||
const AuthorInfo = ({ listing, onCategoryClick }) => {
|
||||
const { category, location, author = {} } = listing;
|
||||
const { username, name } = author;
|
||||
return (
|
||||
<div className="single-classified-listing-author-info">
|
||||
<a
|
||||
href={`/listings/${category}`}
|
||||
onClick={e => onCategoryClick(e, category)}
|
||||
data-no-instant
|
||||
>
|
||||
{category}
|
||||
</a>
|
||||
<LocationText location={location} />
|
||||
・
|
||||
<a href={`/${username}`}>{name}</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuthorInfo.propTypes = {
|
||||
listing: listingPropTypes.isRequired,
|
||||
onCategoryClick: PropTypes.func,
|
||||
};
|
||||
|
||||
AuthorInfo.defaultProps = {
|
||||
onCategoryClick: () => {},
|
||||
};
|
||||
|
||||
export default AuthorInfo;
|
||||
91
app/javascript/listings/singleListing/DropdownMenu.jsx
Normal file
91
app/javascript/listings/singleListing/DropdownMenu.jsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import ThreeDotsIcon from 'images/three-dots.svg';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
|
||||
const MenuButton = ({ onClick }) => (
|
||||
<button
|
||||
type="button"
|
||||
className="dropdown-btn"
|
||||
aria-label="Toggle dropdown menu"
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
src={ThreeDotsIcon}
|
||||
className="dropdown-icon"
|
||||
alt="Dropdown menu icon"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
MenuButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class DropdownMenu extends Component {
|
||||
componentRef = createRef();
|
||||
|
||||
static propTypes = {
|
||||
isOwner: PropTypes.bool.isRequired,
|
||||
listing: listingPropTypes.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleMenu = () => {
|
||||
const { isOpen } = this.state;
|
||||
this.setState({ isOpen: !isOpen }, this.addOrRemoveClickOutsideHandler);
|
||||
};
|
||||
|
||||
addOrRemoveClickOutsideHandler = () => {
|
||||
const { isOpen } = this.state;
|
||||
return isOpen
|
||||
? document.addEventListener('mousedown', this.handleClickOutside)
|
||||
: document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
};
|
||||
|
||||
handleClickOutside = e => {
|
||||
if (
|
||||
this.componentRef.current &&
|
||||
!this.componentRef.current.contains(e.target)
|
||||
) {
|
||||
this.toggleMenu();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { listing, isOwner } = this.props;
|
||||
const { isOpen } = this.state;
|
||||
const { id, category, slug } = listing;
|
||||
const editUrl = `/listings/${id}/edit`;
|
||||
const reportUrl = `/report-abuse?url=https://dev.to/listings/${category}/${slug}`;
|
||||
|
||||
return (
|
||||
<div className="dropdown-menu" ref={this.componentRef}>
|
||||
<MenuButton onClick={this.toggleMenu} />
|
||||
<div className="dropdown">
|
||||
<div
|
||||
className={['dropdown-content', isOpen ? 'showing' : ''].join(' ')}
|
||||
>
|
||||
{isOwner ? (
|
||||
<a href={editUrl} className="classified-listing-edit-button">
|
||||
Edit
|
||||
</a>
|
||||
) : (
|
||||
<a href={reportUrl}>Report Abuse</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DropdownMenu;
|
||||
34
app/javascript/listings/singleListing/Header.jsx
Normal file
34
app/javascript/listings/singleListing/Header.jsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
import DropdownMenu from './DropdownMenu';
|
||||
|
||||
const Header = ({ listing, currentUserId, onTitleClick }) => {
|
||||
const { id, user_id: userId, category, slug, title } = listing;
|
||||
return (
|
||||
<h3 className="single-classified-listing-header">
|
||||
<a
|
||||
href={`/listings/${category}/${slug}`}
|
||||
data-no-instant
|
||||
onClick={e => onTitleClick(e, listing)}
|
||||
data-listing-id={id}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
|
||||
<DropdownMenu listing={listing} isOwner={currentUserId === userId} />
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
listing: listingPropTypes.isRequired,
|
||||
currentUserId: PropTypes.number,
|
||||
onTitleClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
currentUserId: null,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
31
app/javascript/listings/singleListing/TagLinks.jsx
Normal file
31
app/javascript/listings/singleListing/TagLinks.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
const TagLinks = ({ tags, onClick }) => (
|
||||
<div className="single-classified-listing-tags">
|
||||
{tags.length
|
||||
? tags.map(tag => {
|
||||
return (
|
||||
<a
|
||||
href={`/listings?t=${tag}`}
|
||||
onClick={e => onClick(e, tag)}
|
||||
data-no-instant
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
|
||||
TagLinks.propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
TagLinks.defaultProps = {
|
||||
tags: [],
|
||||
};
|
||||
|
||||
export default TagLinks;
|
||||
56
app/javascript/listings/singleListing/index.jsx
Normal file
56
app/javascript/listings/singleListing/index.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
import Header from './Header';
|
||||
import TagLinks from './TagLinks';
|
||||
import AuthorInfo from './AuthorInfo';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
|
||||
const SingleListing = ({
|
||||
listing,
|
||||
currentUserId,
|
||||
onAddTag,
|
||||
onChangeCategory,
|
||||
onOpenModal,
|
||||
isOpen,
|
||||
}) => {
|
||||
const definedClass = isOpen
|
||||
? 'single-classified-listing single-classified-listing--opened'
|
||||
: 'single-classified-listing';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={definedClass}
|
||||
id={`single-classified-listing-${listing.id}`}
|
||||
>
|
||||
<div className="listing-content">
|
||||
<Header
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onTitleClick={onOpenModal}
|
||||
/>
|
||||
<div
|
||||
className="single-classified-listing-body"
|
||||
dangerouslySetInnerHTML={{ __html: listing.processed_html }} // eslint-disable-line react/no-danger
|
||||
/>
|
||||
<TagLinks tags={listing.tag_list} onClick={onAddTag} />
|
||||
<AuthorInfo listing={listing} onCategoryClick={onChangeCategory} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SingleListing.propTypes = {
|
||||
listing: listingPropTypes.isRequired,
|
||||
onAddTag: PropTypes.func.isRequired,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
onChangeCategory: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.number,
|
||||
};
|
||||
|
||||
SingleListing.defaultProps = {
|
||||
currentUserId: null,
|
||||
};
|
||||
|
||||
export default SingleListing;
|
||||
18
app/javascript/listings/singleListing/listingPropTypes.js
Normal file
18
app/javascript/listings/singleListing/listingPropTypes.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export default PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
category: PropTypes.string,
|
||||
contact_via_connect: PropTypes.bool,
|
||||
location: PropTypes.string,
|
||||
processed_html: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
user_id: PropTypes.number,
|
||||
tag_list: PropTypes.arrayOf(PropTypes.string),
|
||||
author: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
}),
|
||||
});
|
||||
|
|
@ -1234,4 +1234,4 @@ ActiveRecord::Schema.define(version: 2020_02_13_182938) do
|
|||
add_foreign_key "users_roles", "users", on_delete: :cascade
|
||||
add_foreign_key "webhook_endpoints", "oauth_applications"
|
||||
add_foreign_key "webhook_endpoints", "users"
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue