Listings redesign (#9349)
* flare tag line height * . * dropdown fix + actions bar fix * actions bar on mob * . * css commented out * . * button fix * preload adjustments * tags * Fixed listings modal to make it semantic HTML via <dialog />. * Refactor an a11y fix. * Removed test that is no longer needed. * Added missing global function for test.. * Fixed broken <ListingFiltersCategories /> tests. * Refactor of mobile dropdown for listing categories. * Updated a TODO. * Made the clear query button a crayons Preact button. * padding * Fixed listings modal for accessiblilty. * Removed rspecs that are no longer valid. * We're no longer using the <dialog /> element, at least for now, so CSS changes aren't necessary. * Wasn't supposed to be committed. Co-authored-by: Nick Taylor <nick@dev.to> Co-authored-by: rhymes <rhymes@hey.com> Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
This commit is contained in:
parent
11820aa29b
commit
313cd61ee3
30 changed files with 624 additions and 824 deletions
|
|
@ -28,4 +28,5 @@
|
|||
|
||||
@import 'views/article';
|
||||
@import 'views/article-form';
|
||||
@import 'views/listings';
|
||||
@import 'views/dashboard';
|
||||
|
|
|
|||
|
|
@ -2,292 +2,163 @@
|
|||
@import 'mixins';
|
||||
|
||||
.listings-container {
|
||||
margin: 24px 0;
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
min-height: 100vh;
|
||||
.listing-filters {
|
||||
padding: 0 16px;
|
||||
a {
|
||||
padding: 3px 16px;
|
||||
border-radius: 100px;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
&.selected {
|
||||
background: $bold-blue;
|
||||
color: white;
|
||||
&.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;
|
||||
}
|
||||
&.new-listing-button {
|
||||
color: $black;
|
||||
background: $green;
|
||||
margin: 0px 4px;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.listing-filters-categories {
|
||||
a {
|
||||
display: block;
|
||||
padding: 7px 16px;
|
||||
&.listing-create-link {
|
||||
border: 2px solid darken($green, 14%);
|
||||
color: darken($green, 19%);
|
||||
padding: 6px 16px;
|
||||
margin-top: 6px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 550px) {
|
||||
a {
|
||||
display: inline-block;
|
||||
margin: 0 8px 8px 0;
|
||||
border: 1px solid $bold-blue;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
padding: 3px 16px;
|
||||
&.listing-create-link {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listing-filters-tags {
|
||||
display: flex;
|
||||
padding: 20px 0px;
|
||||
position: relative;
|
||||
input {
|
||||
.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);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
$light-gray
|
||||
&[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;
|
||||
padding: 3px 14px;
|
||||
border: 1px solid $light-medium-gray;
|
||||
width: calc(100% - 26px);
|
||||
margin-bottom: 4px;
|
||||
@media screen and (min-width: 550px) {
|
||||
width: 280px;
|
||||
margin-right: 8px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.listing-search-clear {
|
||||
background: $light-gray;
|
||||
border: 1px solid $light-medium-gray;
|
||||
position: absolute;
|
||||
left: calc(100% - 25px);
|
||||
top: 24px;
|
||||
border-radius: 8px;
|
||||
@media screen and (min-width: 550px) {
|
||||
left: 282px;
|
||||
}
|
||||
}
|
||||
.listing-tag {
|
||||
font-size: 0.7em;
|
||||
color: $black;
|
||||
border-radius: 100px;
|
||||
display: inline-block;
|
||||
background: $dark-gray;
|
||||
margin-right: 5px;
|
||||
a {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
&.clear-all {
|
||||
background: $light-medium-gray;
|
||||
a {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: white;
|
||||
padding: 3px 2px;
|
||||
.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;
|
||||
&.tag-name {
|
||||
padding-right: 0px;
|
||||
margin: 20px;
|
||||
select {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listings-columns {
|
||||
display: grid;
|
||||
grid-gap: 24px;
|
||||
padding: 0 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
|
||||
grid-auto-rows: 0;
|
||||
}
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
display: inline-block;
|
||||
color: var(--link-brand-color);
|
||||
.listings-unpublish {
|
||||
background: $red;
|
||||
}
|
||||
@media screen and (min-width: 580px) {
|
||||
font-size: 1.1em;
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
color: var(--card-color-tertiary);
|
||||
}
|
||||
}
|
||||
.listings-unpublish {
|
||||
background: $red;
|
||||
}
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
color: var(--card-color-tertiary);
|
||||
}
|
||||
}
|
||||
.listings-load-more-button {
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
background: var(--theme-container-background);
|
||||
color: var(--body-color);
|
||||
border: 1px solid var(--card-color-tertiary);
|
||||
font-size: 17px;
|
||||
padding: 14px 5px;
|
||||
margin: 40px auto 70px;
|
||||
width: 320px;
|
||||
max-width: 80%;
|
||||
border-radius: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.field {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,230 +214,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.single-listing-container {
|
||||
width: 100%;
|
||||
z-index: 20;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0px;
|
||||
margin: 12px auto 0px;
|
||||
overflow: hidden;
|
||||
.single-listing-container__inner {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
box-sizing: content-box;
|
||||
padding-right: 17px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-top: 95px;
|
||||
margin-bottom: 150px;
|
||||
@media screen and (min-width: 950px) {
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.single-listing-container__spacer {
|
||||
height: 150px;
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
}
|
||||
.single-listing-container__inner::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.single-listing {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
border: 1px solid $light-medium-gray;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.9em;
|
||||
break-inside: avoid;
|
||||
background: var(--card-bg);
|
||||
h3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0px;
|
||||
padding: 9px 3px 9px 12px;
|
||||
position: relative;
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
lighten($light-medium-gray, 8%)
|
||||
);
|
||||
border-bottom: 1px solid $light-medium-gray;
|
||||
font-size: 16px;
|
||||
a {
|
||||
color: var(--body-color);
|
||||
}
|
||||
}
|
||||
.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-listing-body {
|
||||
padding: 10px 12px;
|
||||
font-size: 15px;
|
||||
.listing-contact-cta {
|
||||
border: 2px solid $black;
|
||||
color: $black;
|
||||
padding: 8px 10px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
border-radius: 200px;
|
||||
margin: 4px auto;
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
.single-listing-tags {
|
||||
padding: 10px 15px;
|
||||
a {
|
||||
color: var(--card-color-tertiary);
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
$light-medium-gray
|
||||
);
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
padding: 2px 7px;
|
||||
font-size: 0.77em;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.single-listing-author-info {
|
||||
font-size: 0.69em;
|
||||
color: var(--card-color-tertiary);
|
||||
padding: 0px 15px 4px;
|
||||
a {
|
||||
color: var(--card-color-tertiary);
|
||||
display: inline-block;
|
||||
&.listing-edit-button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.single-listing--opened {
|
||||
z-index: 20;
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
padding-bottom: 12px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
@media screen and (min-width: 950px) {
|
||||
h3 {
|
||||
font-size: calc(20px + 0.05vw);
|
||||
}
|
||||
.single-listing-body {
|
||||
font-size: calc(18px + 0.05vw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.single-listing-info-link {
|
||||
display: block;
|
||||
width: 320px;
|
||||
max-width: 92%;
|
||||
margin: auto;
|
||||
padding: 14px 0px;
|
||||
text-align: center;
|
||||
border-radius: 100px;
|
||||
font-weight: bold;
|
||||
margin: 40px auto 10px;
|
||||
color: var(--body-color);
|
||||
border: 3px solid var(--body-color);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
form.listings-contact-via-connect {
|
||||
border-radius: 3px;
|
||||
z-index: 20;
|
||||
position: relative;
|
||||
padding: 10px 15px;
|
||||
margin: 12px auto;
|
||||
width: 100%;
|
||||
padding-bottom: 12px;
|
||||
box-shadow: none;
|
||||
max-width: 100%;
|
||||
text-align: right;
|
||||
p {
|
||||
text-align: left;
|
||||
}
|
||||
width: 94%;
|
||||
max-width: 800px;
|
||||
textarea#new-message {
|
||||
height: 100px;
|
||||
margin: 5px auto 10px auto;
|
||||
font-size: 15px;
|
||||
@media screen and (min-width: 950px) {
|
||||
font-size: calc(18px + 0.05vw);
|
||||
}
|
||||
}
|
||||
.submit-button {
|
||||
margin: none 10px;
|
||||
padding: 0.5em;
|
||||
height: 2em;
|
||||
width: 4em;
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
border: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.listings-modal-background {
|
||||
z-index: 15;
|
||||
position: fixed;
|
||||
|
|
|
|||
31
app/assets/stylesheets/views/listings.scss
Normal file
31
app/assets/stylesheets/views/listings.scss
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@import '../variables';
|
||||
@import '../config/import';
|
||||
|
||||
.listings-columns {
|
||||
margin-bottom: var(--su-6);
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
display: grid;
|
||||
grid-gap: var(--su-4);
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.single-listing {
|
||||
$el: &;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
|
||||
&__dropdown {
|
||||
display: none;
|
||||
|
||||
#{$el}:hover & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__tags {
|
||||
min-height: var(--su-4);
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ describe('<AllListings />', () => {
|
|||
|
||||
// Ensure each listing is present
|
||||
const titleOptions = {
|
||||
selector: 'h3 > a',
|
||||
selector: 'h2 > a',
|
||||
};
|
||||
|
||||
// 1st listings
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import { axe } from 'jest-axe';
|
|||
import ListingFiltersCategories from '../components/ListingFiltersCategories';
|
||||
|
||||
describe('<ListingFiltersCategories />', () => {
|
||||
beforeEach(() => {
|
||||
global.selectNavigation = jest.fn();
|
||||
});
|
||||
|
||||
const firstCategory = {
|
||||
slug: 'clojure',
|
||||
name: 'Clojure',
|
||||
|
|
@ -39,9 +43,8 @@ describe('<ListingFiltersCategories />', () => {
|
|||
it('should render an all link', () => {
|
||||
const { getByText } = renderListingFilterCategories();
|
||||
|
||||
const allLink = getByText(/all/i);
|
||||
const allLink = getByText(/all listings/i, { selector: 'a' });
|
||||
expect(allLink.getAttribute('href')).toEqual('/listings');
|
||||
expect(allLink.textContent).toEqual('all');
|
||||
});
|
||||
|
||||
it('should be "selected" when there is no category selected', () => {
|
||||
|
|
@ -52,27 +55,13 @@ describe('<ListingFiltersCategories />', () => {
|
|||
|
||||
expect(queryByTestId('selected')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render a create listing link', () => {
|
||||
const { getByText } = renderListingFilterCategories();
|
||||
const createListing = getByText(/create a listing/i);
|
||||
expect(createListing.getAttribute('href')).toContain('/listings/new');
|
||||
});
|
||||
|
||||
it('should render a manage listings link', () => {
|
||||
const { getByText } = renderListingFilterCategories();
|
||||
const manageListing = getByText(/manage listings/i);
|
||||
expect(manageListing.getAttribute('href')).toContain(
|
||||
'/listings/dashboard',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should render all the categories links', () => {
|
||||
it('should render the categories name and their respective links', () => {
|
||||
const { getByText } = renderListingFilterCategories();
|
||||
categories.forEach((category) => {
|
||||
const categoryLink = getByText(`${category.name}`);
|
||||
const categoryLink = getByText(`${category.name}`, { selector: 'a' });
|
||||
expect(categoryLink.getAttribute('href')).toEqual(
|
||||
`/listings/${category.slug}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,13 +60,6 @@ describe('<MessageModal />', () => {
|
|||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show the personalized message about the interactions', () => {
|
||||
const { getByTestId } = renderMessageModal(listingWithCurrentUserId);
|
||||
expect(
|
||||
getByTestId('personal-message-about-interactions').textContent,
|
||||
).toEqual('All private interactions must abide by the code of conduct');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When current user is not the author', () => {
|
||||
|
|
@ -86,16 +79,5 @@ describe('<MessageModal />', () => {
|
|||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show a generic message about the interactions', () => {
|
||||
const { getByTestId } = renderMessageModal(
|
||||
listingWithDifferentCurrentUserId,
|
||||
);
|
||||
expect(
|
||||
getByTestId('generic-message-about-interactions').textContent,
|
||||
).toEqual(
|
||||
'Message must be relevant and on-topic with the listing. All private interactions must abide by the code of conduct',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ describe('<NextPageButton />', () => {
|
|||
it('should show a button', () => {
|
||||
const { queryByText } = render(<NextPageButton {...defaultProps} />);
|
||||
|
||||
expect(queryByText(/load more listings/i)).toBeDefined();
|
||||
expect(queryByText(/load more/i)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should call the onclick handler', () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByText } = render(<NextPageButton onClick={onClick} />);
|
||||
const button = getByText(/load more listings/i);
|
||||
const button = getByText(/load more/i);
|
||||
|
||||
button.click();
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { h } from 'preact';
|
|||
import { axe } from 'jest-axe';
|
||||
import { render } from '@testing-library/preact';
|
||||
|
||||
import SingleListing from '../singleListing';
|
||||
import { SingleListing } from '../singleListing/SingleListing';
|
||||
|
||||
const listing = {
|
||||
id: 22,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import SingleListing from '../singleListing';
|
||||
import { SingleListing } from '../singleListing/SingleListing';
|
||||
import NextPageButton from './NextPageButton';
|
||||
|
||||
const AllListings = ({
|
||||
listings,
|
||||
|
|
@ -8,20 +9,26 @@ const AllListings = ({
|
|||
onChangeCategory,
|
||||
currentUserId,
|
||||
onOpenModal,
|
||||
showNextPageButton,
|
||||
loadNextPage,
|
||||
}) => {
|
||||
return (
|
||||
<div className="listings-columns" id="listings-results">
|
||||
{listings.map((listing) => (
|
||||
<SingleListing
|
||||
onAddTag={onAddTag}
|
||||
onChangeCategory={onChangeCategory}
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={onOpenModal}
|
||||
isOpen={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<main class="crayons-layout__content">
|
||||
<div className="listings-columns" id="listings-results">
|
||||
{listings.map((listing) => (
|
||||
<SingleListing
|
||||
onAddTag={onAddTag}
|
||||
onChangeCategory={onChangeCategory}
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={onOpenModal}
|
||||
isOpen={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showNextPageButton && <NextPageButton onClick={loadNextPage} />}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -31,6 +38,8 @@ AllListings.propTypes = {
|
|||
onAddTag: PropTypes.func.isRequired,
|
||||
onChangeCategory: PropTypes.func.isRequired,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
showNextPageButton: PropTypes.bool.isRequired,
|
||||
loadNextPage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AllListings.defaultProps = {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ const CategoryLinks = ({ categories, onClick, selectedCategory }) => {
|
|||
<a
|
||||
href={`/listings/${category.slug}`}
|
||||
id={`category-link-${category.slug}`}
|
||||
className={category.slug === selectedCategory ? 'selected' : ''}
|
||||
className={`crayons-link crayons-link--block ${
|
||||
category.slug === selectedCategory ? 'crayons-link--current' : ''
|
||||
}`}
|
||||
onClick={(e) => onClick(e, category.slug)}
|
||||
data-no-instant
|
||||
{...dataTestIdProp}
|
||||
|
|
|
|||
52
app/javascript/listings/components/CategoryLinksMobile.jsx
Normal file
52
app/javascript/listings/components/CategoryLinksMobile.jsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
global selectNavigation
|
||||
*/
|
||||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export class CategoryLinks extends Component {
|
||||
componentDidMount() {
|
||||
selectNavigation('mobile_nav_listings');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { categories, selectedCategory } = this.props;
|
||||
|
||||
return (
|
||||
<div className="block m:hidden">
|
||||
<select
|
||||
id="mobile_nav_listings"
|
||||
class="crayons-select"
|
||||
aria-label="Listings"
|
||||
>
|
||||
<option value="/listings" selected={selectedCategory === ''}>
|
||||
All listings
|
||||
</option>
|
||||
|
||||
{categories.map((category) => {
|
||||
return (
|
||||
<option
|
||||
value={`/listings/${category.slug}`}
|
||||
selected={category.slug === selectedCategory}
|
||||
>
|
||||
{category.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CategoryLinks.propTypes = {
|
||||
categories: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
slug: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
selectedCategory: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CategoryLinks;
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons/Button/Button';
|
||||
|
||||
const ClearQueryButton = ({ onClick }) => (
|
||||
<button
|
||||
<Button
|
||||
data-testid="clear-query-button"
|
||||
type="button"
|
||||
className="listing-search-clear"
|
||||
variant="ghost"
|
||||
className="absolute right-0"
|
||||
onClick={onClick}
|
||||
id="clear-query-button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
|
||||
ClearQueryButton.propTypes = {
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ const ListingFilters = ({
|
|||
query,
|
||||
}) => {
|
||||
return (
|
||||
<div className="listing-filters" id="listing-filters">
|
||||
<ListingFiltersCategories
|
||||
categories={categories}
|
||||
category={category}
|
||||
onClick={onSelectCategory}
|
||||
/>
|
||||
<aside className="crayons-layout__sidebar-left">
|
||||
<ListingFiltersTags
|
||||
message={message}
|
||||
onKeyUp={onKeyUp}
|
||||
|
|
@ -31,7 +26,12 @@ const ListingFilters = ({
|
|||
onKeyPress={onKeyPress}
|
||||
query={query}
|
||||
/>
|
||||
</div>
|
||||
<ListingFiltersCategories
|
||||
categories={categories}
|
||||
category={category}
|
||||
onClick={onSelectCategory}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,30 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import CategoryLinks from './CategoryLinks';
|
||||
import CategoryLinksMobile from './CategoryLinksMobile';
|
||||
|
||||
const ListingFiltersCategories = ({ categories, category, onClick }) => (
|
||||
<div className="listing-filters-categories">
|
||||
<a
|
||||
id="listings-link"
|
||||
href="/listings"
|
||||
className={category === '' ? 'selected' : ''}
|
||||
data-testid={category === '' ? 'selected' : ''}
|
||||
onClick={onClick}
|
||||
data-no-instant
|
||||
>
|
||||
all
|
||||
</a>
|
||||
<CategoryLinks
|
||||
categories={categories}
|
||||
onClick={onClick}
|
||||
selectedCategory={category}
|
||||
/>
|
||||
<a
|
||||
id="listings-new-link"
|
||||
href="/listings/new"
|
||||
className="listing-create-link"
|
||||
>
|
||||
Create a Listing
|
||||
</a>
|
||||
<a
|
||||
id="listings-dashboard-link"
|
||||
href="/listings/dashboard"
|
||||
className="listing-create-link"
|
||||
>
|
||||
Manage Listings
|
||||
</a>
|
||||
<div className="listing-filters px-2 m:px-0" id="listing-filters">
|
||||
<nav className="hidden m:block">
|
||||
<a
|
||||
id="listings-link"
|
||||
href="/listings"
|
||||
className={`crayons-link crayons-link--block ${
|
||||
category === '' ? 'crayons-link--current' : ''
|
||||
}`}
|
||||
data-testid={category === '' ? 'selected' : ''}
|
||||
onClick={onClick}
|
||||
data-no-instant
|
||||
>
|
||||
All listings
|
||||
</a>
|
||||
<CategoryLinks
|
||||
categories={categories}
|
||||
onClick={onClick}
|
||||
selectedCategory={category}
|
||||
/>
|
||||
</nav>
|
||||
<CategoryLinksMobile categories={categories} selectedCategory={category} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ const ListingFiltersTags = ({
|
|||
const shouldRenderClearQueryButton = query.length > 0;
|
||||
|
||||
return (
|
||||
<div className="listing-filters-tags" id="listing-filters-tags">
|
||||
<div className="relative pb-2 m:pb-6 px-2 m:px-0">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="search"
|
||||
placeholder="Search..."
|
||||
id="listings-search"
|
||||
aria-label="Search listings"
|
||||
autoComplete="off"
|
||||
className="crayons-textfield"
|
||||
defaultValue={message}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const MessageModal = ({
|
||||
currentUserId,
|
||||
|
|
@ -14,58 +15,48 @@ const MessageModal = ({
|
|||
<form
|
||||
data-testid="listings-message-modal"
|
||||
id="listings-message-form"
|
||||
className="listings-contact-via-connect"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{isCurrentUserOnListing ? (
|
||||
<p id="personal-contact-message">
|
||||
This is your active listing. Any member can contact you via this form.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<b id="generic-contact-message">
|
||||
Contact
|
||||
{` ${listing.author.name} `}
|
||||
via DEV Connect
|
||||
</b>
|
||||
</p>
|
||||
)}
|
||||
<header className="mb-4">
|
||||
<h2 className="fs-xl fw-bold lh-tight">Interested?</h2>
|
||||
{isCurrentUserOnListing ? (
|
||||
<p className="color-base-70">
|
||||
This is your active listing. Any member can contact you via this
|
||||
form.
|
||||
</p>
|
||||
) : (
|
||||
<p className="color-base-70">Message {` ${listing.author.name} `}</p>
|
||||
)}
|
||||
</header>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={onChangeDraftingMessage}
|
||||
data-testid="listing-new-message"
|
||||
id="new-message"
|
||||
rows="4"
|
||||
cols="70"
|
||||
className="crayons-textfield mb-0"
|
||||
placeholder="Enter your message here..."
|
||||
/>
|
||||
<button type="submit" value="Submit" className="submit-button cta">
|
||||
SEND
|
||||
</button>
|
||||
<p>
|
||||
{isCurrentUserOnListing ? (
|
||||
<em data-testid="personal-message-about-interactions" id="personal-message-about-interactions">
|
||||
All private interactions
|
||||
{' '}
|
||||
<b>must</b>
|
||||
{' '}
|
||||
abide by the
|
||||
{' '}
|
||||
<a href="/code-of-conduct">code of conduct</a>
|
||||
</em>
|
||||
) : (
|
||||
<em data-testid="generic-message-about-interactions" id="generic-message-about-interactions">
|
||||
Message must be relevant and on-topic with the listing. All private
|
||||
interactions
|
||||
{' '}
|
||||
<b>must</b>
|
||||
{' '}
|
||||
abide by the
|
||||
{' '}
|
||||
<a href="/code-of-conduct">code of conduct</a>
|
||||
</em>
|
||||
)}
|
||||
<p
|
||||
className="mb-4 fs-s color-base-60"
|
||||
>
|
||||
{isCurrentUserOnListing &&
|
||||
'Message must be relevant and on-topic with the listing.'}
|
||||
All private interactions <b>must</b> abide by the{' '}
|
||||
<a href="/code-of-conduct" className="crayons-link crayons-link--brand">
|
||||
Code of Conduct
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mr-2"
|
||||
tagName="button"
|
||||
type="submit"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import SingleListing from '../singleListing';
|
||||
import { SingleListing } from '../singleListing/SingleListing';
|
||||
import MessageModal from './MessageModal';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const Modal = ({
|
||||
currentUserId,
|
||||
|
|
@ -16,36 +17,60 @@ const Modal = ({
|
|||
}) => {
|
||||
const shouldRenderMessageModal = listing && listing.contact_via_connect;
|
||||
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// TODO: Why are we not using the crayons modal component and instead recreating it here?
|
||||
return (
|
||||
<div className="single-listing-container">
|
||||
<div
|
||||
id="single-listing-container__inner"
|
||||
className="single-listing-container__inner"
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
onKeyPress={onClick}
|
||||
tabIndex="0"
|
||||
>
|
||||
<SingleListing
|
||||
onAddTag={onAddTag}
|
||||
onChangeCategory={onChangeCategory}
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={onOpenModal}
|
||||
isOpen
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
id="single-listing-container__inner"
|
||||
className="single-listing-container__inner crayons-modal__box"
|
||||
>
|
||||
<div className="crayons-modal__box__header flex s:hidden">
|
||||
<Button
|
||||
type="button"
|
||||
id="close-listing-modal"
|
||||
tagName="button"
|
||||
contentType="icon"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
icon={Icon}
|
||||
onClick={onClick}
|
||||
aria-label="Close listing"
|
||||
/>
|
||||
{shouldRenderMessageModal && (
|
||||
<MessageModal
|
||||
onSubmit={onSubmit}
|
||||
onChangeDraftingMessage={onChangeDraftingMessage}
|
||||
message={message}
|
||||
</div>
|
||||
<div className="crayons-modal__box__body p-0">
|
||||
<div className="p-3 m:p-6 l:p-8">
|
||||
<SingleListing
|
||||
onAddTag={onAddTag}
|
||||
onChangeCategory={onChangeCategory}
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={onOpenModal}
|
||||
isOpen
|
||||
/>
|
||||
</div>
|
||||
{shouldRenderMessageModal && (
|
||||
<div className="bg-base-10 p-3 m:p-6 l:p-8">
|
||||
<MessageModal
|
||||
onSubmit={onSubmit}
|
||||
onChangeDraftingMessage={onChangeDraftingMessage}
|
||||
message={message}
|
||||
listing={listing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<a href="/about-listings" className="single-listing-info-link">
|
||||
About DEV Listings
|
||||
</a>
|
||||
<div className="single-listing-container__spacer" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||
const ModalBackground = ({ onClick }) => (
|
||||
<div
|
||||
data-testid="listings-modal-background"
|
||||
className="listings-modal-background"
|
||||
className="crayons-modal__overlay"
|
||||
onClick={onClick}
|
||||
role="presentation"
|
||||
id="listings-modal-background"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const NextPageButton = ({ onClick }) => (
|
||||
<div className="listings-load-more-button">
|
||||
<button onClick={onClick} type="button">
|
||||
Load More Listings
|
||||
</button>
|
||||
<div className="flex justify-center">
|
||||
<Button variant="secondary" onClick={onClick} type="button">
|
||||
Load more...
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ import { selectedTagsPropTypes } from '../../common-prop-types';
|
|||
|
||||
const SelectedTags = ({ tags, onRemoveTag, onKeyPress }) => {
|
||||
return (
|
||||
<section>
|
||||
<section class="pt-2">
|
||||
{tags.map((tag) => (
|
||||
<span className="listing-tag" key={tag.id} id={`selected-tag-${tag}`}>
|
||||
<a href={`/listings?t=${tag}`} className="tag-name" data-no-instant>
|
||||
<span className="listing-tag mr-1" key={tag.id} id={`selected-tag-${tag}`}>
|
||||
<a href={`/listings?t=${tag}`} className="tag-name crayons-tag" data-no-instant>
|
||||
<span className="crayons-tag__prefix">#</span>
|
||||
<span role="button" tabIndex="0">
|
||||
{tag}
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="px-1"
|
||||
onClick={(e) => onRemoveTag(e, tag)}
|
||||
onKeyPress={(e) => onKeyPress(e, tag)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { fetchSearch } from '../utilities/search';
|
|||
import ModalBackground from './components/ModalBackground';
|
||||
import Modal from './components/Modal';
|
||||
import AllListings from './components/AllListings';
|
||||
import NextPageButton from './components/NextPageButton';
|
||||
import ListingFilters from './components/ListingFilters';
|
||||
import {
|
||||
LISTING_PAGE_SIZE,
|
||||
|
|
@ -289,10 +288,7 @@ export class Listings extends Component {
|
|||
this.triggerMasonry();
|
||||
}
|
||||
return (
|
||||
<div className="listings__container">
|
||||
{shouldRenderModal && (
|
||||
<ModalBackground onClick={this.handleCloseModal} />
|
||||
)}
|
||||
<div className="crayons-layout crayons-layout--2-cols">
|
||||
<ListingFilters
|
||||
categories={allCategories}
|
||||
category={category}
|
||||
|
|
@ -311,20 +307,24 @@ export class Listings extends Component {
|
|||
onChangeCategory={this.selectCategory}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
showNextPageButton={showNextPageButton}
|
||||
loadNextPage={this.loadNextPage}
|
||||
/>
|
||||
{showNextPageButton && <NextPageButton onClick={this.loadNextPage} />}
|
||||
{shouldRenderModal && (
|
||||
<Modal
|
||||
currentUserId={currentUserId}
|
||||
onAddTag={this.addTag}
|
||||
onChangeDraftingMessage={this.handleDraftingMessage}
|
||||
onClick={this.handleCloseModal}
|
||||
onChangeCategory={this.selectCategory}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
onSubmit={this.handleSubmitMessage}
|
||||
listing={openedListing}
|
||||
message={message}
|
||||
/>
|
||||
<div className="crayons-modal">
|
||||
<Modal
|
||||
currentUserId={currentUserId}
|
||||
onAddTag={this.addTag}
|
||||
onChangeDraftingMessage={this.handleDraftingMessage}
|
||||
onClick={this.handleCloseModal}
|
||||
onChangeCategory={this.selectCategory}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
onSubmit={this.handleSubmitMessage}
|
||||
listing={openedListing}
|
||||
message={message}
|
||||
/>
|
||||
<ModalBackground onClick={this.handleCloseModal} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ import listingPropTypes from './listingPropTypes';
|
|||
|
||||
const LocationText = ({ location }) => {
|
||||
return location ? (
|
||||
<a data-testid="single-listing-location" href={`/listings/?q=${location}`}>
|
||||
<a
|
||||
data-testid="single-listing-location"
|
||||
className="crayons-link crayons-link--secondary"
|
||||
href={`/listings/?q=${location}`}
|
||||
>
|
||||
{'・'}
|
||||
{location}
|
||||
</a>
|
||||
|
|
@ -23,18 +27,42 @@ LocationText.defaultProps = {
|
|||
|
||||
const AuthorInfo = ({ listing, onCategoryClick }) => {
|
||||
const { category, location, author = {} } = listing;
|
||||
const { username, name } = author;
|
||||
const { username, name, profile_image_90 } = author;
|
||||
return (
|
||||
<div className="single-listing-author-info">
|
||||
<footer className="fs-s flex items-center">
|
||||
<a
|
||||
href={`/listings/${category}`}
|
||||
onClick={(e) => onCategoryClick(e, category)}
|
||||
data-no-instant
|
||||
href={`/${username}`}
|
||||
className="crayons-avatar crayons-avatar--l mr-2"
|
||||
>
|
||||
{category}
|
||||
<img
|
||||
src={profile_image_90}
|
||||
alt={name}
|
||||
width="32"
|
||||
height="32"
|
||||
className="crayons-avatar__image"
|
||||
/>
|
||||
</a>
|
||||
<LocationText location={location} />・<a href={`/${username}`}>{name}</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
href={`/${username}`}
|
||||
className="crayons-link fw-medium"
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
<p className="fs-xs">
|
||||
<a
|
||||
href={`/listings/${category}`}
|
||||
onClick={(e) => onCategoryClick(e, category)}
|
||||
data-no-instant
|
||||
className="crayons-link crayons-link--secondary"
|
||||
>
|
||||
{category}
|
||||
</a>
|
||||
<LocationText location={location} />
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import ThreeDotsIcon from 'images/overflow-horizontal.svg';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const MenuButton = ({ onClick }) => (
|
||||
<button
|
||||
type="button"
|
||||
className="dropdown-btn"
|
||||
aria-label="Toggle dropdown menu"
|
||||
onClick={onClick}
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<img
|
||||
src={ThreeDotsIcon}
|
||||
className="dropdown-icon"
|
||||
alt="Dropdown menu icon"
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7 12a2 2 0 11-4 0 2 2 0 014 0zm7 0a2 2 0 11-4 0 2 2 0 014 0zm5 2a2 2 0 100-4 2 2 0 000 4z"
|
||||
/>
|
||||
</button>
|
||||
</svg>
|
||||
);
|
||||
|
||||
MenuButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class DropdownMenu extends Component {
|
||||
componentRef = createRef();
|
||||
|
||||
|
|
@ -68,20 +64,29 @@ class DropdownMenu extends Component {
|
|||
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="listing-edit-button">
|
||||
Edit
|
||||
</a>
|
||||
) : (
|
||||
<a href={reportUrl}>Report Abuse</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-listing__dropdown absolute right-0 top-0" ref={this.componentRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
tagName="button"
|
||||
aria-label="Toggle dropdown menu"
|
||||
icon={Icon}
|
||||
onClick={this.toggleMenu}
|
||||
/>
|
||||
<div
|
||||
className={`crayons-dropdown absolute right-0 top-100 p-1 ${
|
||||
isOpen ? 'block' : ''
|
||||
}`}
|
||||
>
|
||||
{isOwner ? (
|
||||
<a href={editUrl} className="crayons-link crayons-link--block">
|
||||
Edit
|
||||
</a>
|
||||
) : (
|
||||
<a href={reportUrl} className="crayons-link crayons-link--block">
|
||||
Report Abuse
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,27 +2,34 @@ import PropTypes from 'prop-types';
|
|||
import { h } from 'preact';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
import DropdownMenu from './DropdownMenu';
|
||||
import TagLinks from './TagLinks';
|
||||
|
||||
const Header = ({ listing, currentUserId, onTitleClick }) => {
|
||||
const Header = ({ listing, currentUserId, onTitleClick, onAddTag }) => {
|
||||
const { id, user_id: userId, category, slug, title } = listing;
|
||||
return (
|
||||
<h3 className="single-listing-header">
|
||||
<a
|
||||
href={`/listings/${category}/${slug}`}
|
||||
data-no-instant
|
||||
onClick={(e) => onTitleClick(e, listing)}
|
||||
data-listing-id={id}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
<header className="mb-3">
|
||||
<h2 className="fs-2xl fw-bold lh-tight mb-1 pr-8">
|
||||
<a
|
||||
href={`/listings/${category}/${slug}`}
|
||||
data-no-instant
|
||||
className="crayons-link"
|
||||
onClick={(e) => onTitleClick(e, listing)}
|
||||
data-listing-id={id}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<TagLinks tags={listing.tags} onClick={onAddTag} />
|
||||
|
||||
<DropdownMenu listing={listing} isOwner={currentUserId === userId} />
|
||||
</h3>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
listing: listingPropTypes.isRequired,
|
||||
onAddTag: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.number,
|
||||
onTitleClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
|||
104
app/javascript/listings/singleListing/SingleListing.jsx
Normal file
104
app/javascript/listings/singleListing/SingleListing.jsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Header from './Header';
|
||||
import AuthorInfo from './AuthorInfo';
|
||||
import listingPropTypes from './listingPropTypes';
|
||||
|
||||
export class SingleListing extends Component {
|
||||
|
||||
listingContent = (listing, currentUserId, onChangeCategory, onOpenModal, onAddTag) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Header
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onTitleClick={onOpenModal}
|
||||
onAddTag={onAddTag}
|
||||
/>
|
||||
<div
|
||||
className="mb-4"
|
||||
dangerouslySetInnerHTML={{ __html: listing.processed_html }} // eslint-disable-line react/no-danger
|
||||
/>
|
||||
<AuthorInfo listing={listing} onCategoryClick={onChangeCategory} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
listingInline = (listing, currentUserId, onChangeCategory, onOpenModal, onAddTag) => {
|
||||
return (
|
||||
<div
|
||||
className="single-listing relative crayons-card"
|
||||
id={`single-listing-${listing.id}`}
|
||||
data-testid={`single-listing-${listing.id}`}
|
||||
>
|
||||
<div className="listing-content p-4">
|
||||
{this.listingContent(
|
||||
listing,
|
||||
currentUserId,
|
||||
onChangeCategory,
|
||||
onOpenModal,
|
||||
onAddTag
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
listingModal = (listing, currentUserId, onChangeCategory, onOpenModal, onAddTag) => {
|
||||
return (
|
||||
<div
|
||||
className="single-listing relative"
|
||||
id={`single-listing-${listing.id}`}
|
||||
data-testid={`single-listing-${listing.id}`}
|
||||
>
|
||||
<div className="listing-content">
|
||||
{this.listingContent(
|
||||
listing,
|
||||
currentUserId,
|
||||
onChangeCategory,
|
||||
onOpenModal,
|
||||
onAddTag,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { listing, currentUserId, onChangeCategory, onOpenModal, isOpen, onAddTag } = this.props;
|
||||
return (
|
||||
isOpen ?
|
||||
this.listingModal(
|
||||
listing,
|
||||
currentUserId,
|
||||
onChangeCategory,
|
||||
onOpenModal,
|
||||
onAddTag
|
||||
)
|
||||
:
|
||||
this.listingInline(
|
||||
listing,
|
||||
currentUserId,
|
||||
onChangeCategory,
|
||||
onOpenModal,
|
||||
onAddTag
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SingleListing.propTypes = {
|
||||
listing: listingPropTypes.isRequired,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
onChangeCategory: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.number,
|
||||
onAddTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SingleListing.defaultProps = {
|
||||
currentUserId: null,
|
||||
};
|
||||
|
||||
SingleListing.displayName = 'SingleListing';
|
||||
|
|
@ -2,15 +2,17 @@ import PropTypes from 'prop-types';
|
|||
import { h } from 'preact';
|
||||
|
||||
const TagLinks = ({ tags, onClick }) => (
|
||||
<div className="single-listing-tags">
|
||||
<div className="-ml-1 single-listing__tags">
|
||||
{tags.length
|
||||
? tags.map((tag) => {
|
||||
return (
|
||||
<a
|
||||
href={`/listings?t=${tag}`}
|
||||
onClick={(e) => onClick(e, tag)}
|
||||
className="crayons-tag"
|
||||
data-no-instant
|
||||
>
|
||||
<span className="crayons-tag__prefix">#</span>
|
||||
{tag}
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
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-listing single-listing--opened'
|
||||
: 'single-listing';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={definedClass}
|
||||
id={`single-listing-${listing.id}`}
|
||||
data-testid={`single-listing-${listing.id}`}
|
||||
>
|
||||
<div className="listing-content">
|
||||
<Header
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onTitleClick={onOpenModal}
|
||||
/>
|
||||
<div
|
||||
className="single-listing-body"
|
||||
dangerouslySetInnerHTML={{ __html: listing.processed_html }} // eslint-disable-line react/no-danger
|
||||
/>
|
||||
<TagLinks tags={listing.tags} 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;
|
||||
|
|
@ -8,6 +8,7 @@ export const MATCH_LISTING = [
|
|||
'single-listing-container__inner',
|
||||
'listing-filters',
|
||||
'listings-modal-background',
|
||||
'close-listing-modal',
|
||||
];
|
||||
|
||||
export function updateListings(listings) {
|
||||
|
|
@ -45,11 +46,7 @@ function resizeMasonryItem(item) {
|
|||
window.getComputedStyle(grid).getPropertyValue('grid-row-gap'),
|
||||
10,
|
||||
);
|
||||
const rowHeight = parseInt(
|
||||
window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'),
|
||||
10,
|
||||
);
|
||||
|
||||
const rowHeight = 0;
|
||||
const rowSpan = Math.ceil(
|
||||
(item.querySelector('.listing-content').getBoundingClientRect().height +
|
||||
rowGap) /
|
||||
|
|
|
|||
|
|
@ -26,15 +26,22 @@
|
|||
<meta name="twitter:card" content="summary_large_image">
|
||||
<% end %>
|
||||
|
||||
<main class="home">
|
||||
<h1 class="visually-hidden-header">Listings</h1>
|
||||
<section class="listings-container" id="listings-index-container"
|
||||
data-category="<%= params[:category] %>" data-listings="<%= @listings_json %>"
|
||||
data-allcategories="<%= categories_for_display.to_json %>"
|
||||
<% if @displayed_listing %>
|
||||
data-displayedlisting="<%= @displayed_listing_json %> "
|
||||
<% end %>>
|
||||
</section>
|
||||
</main>
|
||||
<header class="crayons-layout flex items-center justify-between p-2 l:p-4 l:pb-0">
|
||||
<h1 class="">Listings</h1>
|
||||
|
||||
<div class="flex items-center">
|
||||
<a href="<%= new_listing_path %>" class="crayons-btn crayons-btn--secondary">Create</a>
|
||||
<a href="<%= listings_dashboard_path %>" class="crayons-btn crayons-btn--secondary ml-2">Manage</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="listings-container" id="listings-index-container"
|
||||
data-category="<%= params[:category] %>" data-listings="<%= @listings_json %>"
|
||||
data-allcategories="<%= categories_for_display.to_json %>"
|
||||
<% if @displayed_listing %>
|
||||
data-displayedlisting="<%= @displayed_listing_json %> "
|
||||
<% end %>
|
||||
>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "listings", defer: true %>
|
||||
|
|
|
|||
|
|
@ -537,25 +537,6 @@ context "when running the specs that were previously in another file" do
|
|||
}
|
||||
end
|
||||
|
||||
describe "GET /listings" do
|
||||
it "has page content" do
|
||||
get "/listings"
|
||||
expect(response.body).to include("listing-filters")
|
||||
end
|
||||
|
||||
it "has page content for category page" do
|
||||
get "/listings/saas"
|
||||
expect(response.body).to include("listing-filters")
|
||||
end
|
||||
end
|
||||
|
||||
describe "GETS /listings/new" do
|
||||
it "has page content" do
|
||||
get "/listings"
|
||||
expect(response.body).to include("listing-filters")
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /listings" do
|
||||
before do
|
||||
create_list(:credit, 20, user_id: user.id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue