diff --git a/app/assets/stylesheets/classified_listings.scss b/app/assets/stylesheets/classified_listings.scss index e05ac5439..07d6a6578 100644 --- a/app/assets/stylesheets/classified_listings.scss +++ b/app/assets/stylesheets/classified_listings.scss @@ -574,3 +574,206 @@ form.listings-contact-via-connect { background: rgba(0, 0, 0, 0.8); user-select: none; } + +.dashboard-listings-container { + padding-top: 80px; + padding-bottom: 80px; + width: 800px; + max-width: 96%; + margin: auto; + + .rounded-btn { + font-size: calc(15px + 0.2vw); + font-family: $helvetica-condensed; + font-stretch: condensed; + padding: 4px 20px; + border-radius: 100px; + margin-right: 5px; + display: inline-block; + width: fit-content; + margin-bottom: 10px; + @include themeable( + color, + theme-color, + $black + ); + @include themeable( + background, + theme-container-background, + lighten($light-medium-gray, 5%) + ); + &:hover { + @include themeable( + background, + theme-container-background-hover, + $light-medium-gray + ); + } + + &.active { + @include themeable( + background, + theme-secondary-color, + $dark-gray + ); + @include themeable( + color, + theme-container-background, + white + ); + } + } + + .dashboard-listings-header-wrapper { + display: flex; + flex-direction: row; + text-align: center; + align-items: center; + justify-content: center; + margin-bottom: 15px; + background: $dark-gray; + @include themeable(box-shadow, theme-container-box-shadow, $bold-shadow); + padding: 15px 0px; + + @media screen and (max-width: 768px) { + flex-direction: column; + } + + .dashboard-listings-header { + width: 43%; + text-align: center; + color: white; + margin: 5px auto; + font-size: calc(1.1em + 0.1vw); + padding: 18px 0px 7px; + padding-left: 10px; + font-family: $monospace; + border: 1px solid $light-gray; + display: block; + text-align: center; + margin: 1.2%; + padding: 1.2%; + padding-bottom: 30px; + + @media screen and (max-width: 768px) { + width: 90%; + } + a { + font-size: 0.9em; + border: 2px solid #2ad7c5; + color: #25c2b2; + padding: 6px 16px; + margin-top: 6px; + border-radius: 100px; + } + } + } + + .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); + padding: 5px 15px; + margin-bottom: 13px; + border-radius: 3px; + position: relative; + background: var(--theme-container-background, #fff); + + .listing-org { + @include themeable( + background, + theme-secondary-color, + darken($light-gray, 6%) + ); + @include themeable(color, theme-container-background, #fff); + margin-right: 5px; + padding: 3px 10px; + border-radius: 100px; + margin-top: 10px; + display: inline-block; + font-size: 0.9em; + } + + h2 { + color: var(--theme-container-color, #0a0a0a); + font-weight: 500; + margin-bottom: 10px; + margin-top: 8px; + font-size: 22px; + width: 100%; + @media screen and (min-width: 800px) { + font-size: 34px; + } + } + + .dashboard-listing-date { + font-size: 0.85em; + padding: 3px 0px 6px; + color: var(--theme-secondary-color, #666666); + } + + .dashboard-listing-category { + padding: 10px 15px; + a { + @include themeable( + color, + theme-secondary-color, + $dark-medium-gray + ); + @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; + } + } + + .dashboard-listing-tags { + font-size: 0.85em; + padding: 3px 0px 6px; + a { + color: var(--theme-container-color, #0a0a0a); + } + } + + .listing-row-actions { + padding: 7px 0px 10px; + + .pill { + background: $bold-blue; + padding: 4px 20px; + border-radius: 100px; + color: white; + margin-right: 5px; + font-size: 14px; + border: 2px solid transparent; + margin-top: 6px; + display: inline-block; + + &.green { + background: $green; + color: $black; + } + + &.red { + background: $red; + } + + &.black { + background: $dark-gray; + } + } + } + + } + + + } +} diff --git a/app/controllers/classified_listings_controller.rb b/app/controllers/classified_listings_controller.rb index 3153207e5..29e674bfc 100644 --- a/app/controllers/classified_listings_controller.rb +++ b/app/controllers/classified_listings_controller.rb @@ -3,7 +3,7 @@ class ClassifiedListingsController < ApplicationController before_action :set_classified_listing, only: %i[edit update] before_action :set_cache_control_headers, only: %i[index] after_action :verify_authorized, only: %i[edit update] - before_action :authenticate_user!, only: %i[edit update new] + before_action :authenticate_user!, only: %i[edit update new dashboard] def index @displayed_classified_listing = ClassifiedListing.find_by!(slug: params[:slug]) if params[:slug] @@ -82,6 +82,13 @@ class ClassifiedListingsController < ApplicationController redirect_to "/listings" end + def dashboard + @classified_listings = ClassifiedListing.where(user_id: current_user.id) + @orgs = Organization.where(id: current_user.organization_memberships.where(type_of_user: "admin").map(&:organization_id)) + @org_listings = ClassifiedListing.where(organization_id: current_user.organization_memberships.where(type_of_user: "admin").map(&:organization_id)) + @user_credits = current_user.unspent_credits_count + end + private def mod_page diff --git a/app/javascript/listings/__tests__/__snapshots__/listingDashboard.test.jsx.snap b/app/javascript/listings/__tests__/__snapshots__/listingDashboard.test.jsx.snap new file mode 100644 index 000000000..8668fdf52 --- /dev/null +++ b/app/javascript/listings/__tests__/__snapshots__/listingDashboard.test.jsx.snap @@ -0,0 +1,134 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should load listing dashboard 1`] = ` +preact-render-spy (1 nodes) +------- +
+ + Personal + + + Yoyodyne + + + Infotrode + +
+
+

Listings

+

Listings Made: 3

+ Create a Listing +
+
+

Credits

+

Credits Available: 3

+ + Buy Credits + +
+
+ +
+ +`; diff --git a/app/javascript/listings/__tests__/listingDashboard.test.jsx b/app/javascript/listings/__tests__/listingDashboard.test.jsx new file mode 100644 index 000000000..db9b15f79 --- /dev/null +++ b/app/javascript/listings/__tests__/listingDashboard.test.jsx @@ -0,0 +1,113 @@ +import { h } from 'preact'; +import { deep } from 'preact-render-spy'; +import { JSDOM } from 'jsdom'; +import { ListingDashboard } from '../listingDashboard'; + +const doc = new JSDOM(''); +global.document = doc; +const l = [{id:23,bumped_at:"2019-06-11T16:45:37.229Z",category:"cfp",organization_id:null,slug:"asdfasdf-2ea8",title:"asdfasdf",updated_at:"2019-06-11T16:45:37.237Z",user_id:11,tag_list:["computerscience","career"],author:{name:"MarioSee",username:"mariocsee",profile_image_90:"/uploads/user/profile_image/11/e594d777-b57b-41d6-a793-5d8127bd11b3.jpeg"}},{id:24,bumped_at:"2019-06-11T16:59:16.312Z",category:"events",organization_id:2,slug:"yoyoyoyoyoooooooo-4jcb",title:"YOYOYOYOYOOOOOOOO",updated_at:"2019-06-11T16:59:16.316Z",user_id:11,tag_list:["computerscience","conference","career"],author:{name:"Yoyodyne",username:"org3737",profile_image_90:"/uploads/organization/profile_image/2/5edb1e49-bea9-4e99-bc32-acc10c52a935.png"}},{id:25,bumped_at:"2019-06-11T17:01:25.143Z",category:"cfp",organization_id:3,slug:"hehhehe-5hld",title:"hehhehe",updated_at:"2019-06-11T17:01:25.169Z",user_id:11,tag_list:[],author:{name:"Infotrode",username:"org5254",profile_image_90:"/uploads/organization/profile_image/3/04d4e1f1-c2e0-4147-81e2-bc8a2657296b.png"}}]; +global.document.body.innerHTML = `
`; +global.window = doc.defaultView; + +const listingState = { listings: + [ { id: 23, + bumped_at: '2019-06-11T16:45:37.229Z', + category: 'cfp', + organization_id: null, + slug: 'asdfasdf-2ea8', + title: 'asdfasdf', + updated_at: '2019-06-11T16:45:37.237Z', + user_id: 11, + tag_list: [Array], + author: [Object] }, + { id: 24, + bumped_at: '2019-06-11T16:59:16.312Z', + category: 'events', + organization_id: 2, + slug: 'yoyoyoyoyoooooooo-4jcb', + title: 'YOYOYOYOYOOOOOOOO', + updated_at: '2019-06-11T16:59:16.316Z', + user_id: 11, + tag_list: [Array], + author: [Object] }, + { id: 25, + bumped_at: '2019-06-11T17:01:25.143Z', + category: 'cfp', + organization_id: 3, + slug: 'hehhehe-5hld', + title: 'hehhehe', + updated_at: '2019-06-11T17:01:25.169Z', + user_id: 11, + tag_list: [], + author: [Object] } ], + orgListings: + [ { id: 24, + bumped_at: '2019-06-11T16:59:16.312Z', + category: 'events', + organization_id: 2, + slug: 'yoyoyoyoyoooooooo-4jcb', + title: 'YOYOYOYOYOOOOOOOO', + updated_at: '2019-06-11T16:59:16.316Z', + user_id: 11, + tag_list: [Array], + author: [Object] }, + { id: 25, + bumped_at: '2019-06-11T17:01:25.143Z', + category: 'cfp', + organization_id: 3, + slug: 'hehhehe-5hld', + title: 'hehhehe', + updated_at: '2019-06-11T17:01:25.169Z', + user_id: 11, + tag_list: [], + author: [Object] } ], + orgs: + [ { id: 2, + name: 'Yoyodyne', + slug: 'org3737', + unspent_credits_count: 1 }, + { id: 3, + name: 'Infotrode', + slug: 'org5254', + unspent_credits_count: 1 } ], + userCredits: '3', + selectedListings: 'user' }; + +describe('', () => { + it('should load listing dashboard', () => { + const tree = deep(); + expect(tree).toMatchSnapshot(); + }) + + describe('should load the proper elements', () => { + const context = deep(); + expect(context.component()).toBeInstanceOf(ListingDashboard); + context.setState(listingState); + + it('for user and org buttons', () => { + expect(context.find('.rounded-btn').at(0).text()).toEqual('Personal') + expect(context.find('.rounded-btn').at(1).text()).toEqual(listingState.orgs[0].name) + expect(context.find('.rounded-btn').at(2).text()).toEqual(listingState.orgs[1].name) + + context.find('.rounded-btn').at(1).simulate('click'); + expect(context.find('.rounded-btn').at(1).attr('className')).toEqual('rounded-btn active') + expect(context.state('selectedListings')).toEqual(listingState.orgs[0].id) + }) + + it('for listing and credits header', () => { + expect(context.find('.dashboard-listings-header-wrapper').exists()).toEqual(true); + expect(context.find('.dashboard-listings-header').exists()).toEqual(true); + + expect(context.find('.dashboard-listings-header').at(0).childAt(0).text()).toEqual('Listings'); + expect(context.find('.dashboard-listings-header').at(0).childAt(2).text()).toEqual('Create a Listing'); + + expect(context.find('.dashboard-listings-header').at(1).childAt(0).text()).toEqual('Credits'); + expect(context.find('.dashboard-listings-header').at(1).childAt(2).text()).toEqual('Buy Credits'); + }) + + it('for listingRow view', () => { + expect(context.find('.dashboard-listings-view').exists()).toEqual(true); + expect(context.find('.dashboard-listings-view').children().text()).toEqual(context.find('ListingRow').text()); + }) + }) +}) \ No newline at end of file diff --git a/app/javascript/listings/dashboard/listingRow.jsx b/app/javascript/listings/dashboard/listingRow.jsx new file mode 100644 index 000000000..7142462a5 --- /dev/null +++ b/app/javascript/listings/dashboard/listingRow.jsx @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import { h } from 'preact'; + +export const ListingRow = ({listing}) => { + + const tagLinks = listing.tag_list.map(tag => ( + +# + {tag} + {' '} + + + )); + + const listingDate = listing.bumped_at ? (new Date(listing.bumped_at.toString())).toLocaleDateString("default", {day: "2-digit", month: "short"}) : (new Date(listing.updated_at.toString())).toLocaleDateString("default", {day: "2-digit", month: "short"}) ; + const orgName = (l) => (l.organization_id) ? ({l.author.name}) : ''; + + return( +
+ {orgName(listing)} + +

+ {listing.title} +

+
+ + {listingDate} + {' '} + + {listing.category} + {tagLinks} +
+ {/* BUMP */} + EDIT + {/* DELETE */} +
+
+ ); +} + +ListingRow.propTypes = { + listing: PropTypes.object.isRequired, +}; diff --git a/app/javascript/listings/listingDashboard.jsx b/app/javascript/listings/listingDashboard.jsx new file mode 100644 index 000000000..84976b490 --- /dev/null +++ b/app/javascript/listings/listingDashboard.jsx @@ -0,0 +1,69 @@ +import { h, Component } from 'preact'; +import { ListingRow } from './dashboard/listingRow'; + +export class ListingDashboard extends Component { + state = { + listings: [], + orgListings: [], + orgs: [], + userCredits: 0, + selectedListings: "user", + } + + componentDidMount() { + const t = this; + const container = document.getElementById('classifieds-listings-dashboard') + let listings = []; + let orgs = []; + let orgListings = []; + listings = JSON.parse(container.dataset.listings); + orgs = JSON.parse(container.dataset.orgs); + orgListings = JSON.parse(container.dataset.orglistings); + const userCredits = container.dataset.usercredits; + t.setState({ listings, orgListings, orgs, userCredits }); + } + + render() { + const { listings, orgListings, userCredits, orgs, selectedListings } = this.state + + const showListings = (selected, userListings, organizationListings) => { + return (selected === "user") ? userListings.map(listing => ) : organizationListings.map(listing => (listing.organization_id === selected) ? : ''); + } + + const orgButtons = orgs.map(org => ( + this.setState({selectedListings: org.id})} className={"rounded-btn " + (selectedListings === org.id ? 'active': '')}> + {org.name} + + )) + + const listingLength = (selected, userListings, organizationListings) => { + return (selected === "user") ? (

Listings Made: {userListings.length}

) : (

Listings Made: {organizationListings.filter((listing) => (listing.organization_id == selected)).length}

); + } + + const creditCount = (selected, userCreds, organizations) => { + return (selected === "user") ? (

Credits Available: {userCredits}

) : (

Credits Available: {organizations.find((org) => (org.id === selected)).unspent_credits_count}

); + } + + return ( +
+ this.setState({selectedListings: "user"})} className={"rounded-btn " + (selectedListings === "user" ? 'active': '')}>Personal + {orgButtons} +
+
+

Listings

+ {listingLength(selectedListings, listings, orgListings)} + Create a Listing +
+
+

Credits

+ {creditCount(selectedListings, userCredits, orgs)} + Buy Credits +
+
+
+ {showListings(selectedListings, listings, orgListings)} +
+
+ ) + } +} diff --git a/app/javascript/listings/listings.jsx b/app/javascript/listings/listings.jsx index 3626aae1e..cc59a7861 100644 --- a/app/javascript/listings/listings.jsx +++ b/app/javascript/listings/listings.jsx @@ -22,7 +22,7 @@ export class Listings extends Component { const params = this.getQueryParams(); const t = this; const algoliaId = document.querySelector("meta[name='algolia-public-id']") - .content; + .content; const algoliaKey = document.querySelector("meta[name='algolia-public-key']") .content; const env = document.querySelector("meta[name='environment']").content; @@ -52,9 +52,9 @@ export class Listings extends Component { t.setUser() document.body.addEventListener('keydown', t.handleKeyDown) - - /* - The width of the columns also changes when the browser is resized + + /* + The width of the columns also changes when the browser is resized so we will also call this function on window resize to recalculate each grid item's height to avoid content overflow */ @@ -342,6 +342,7 @@ export class Listings extends Component { this.selectCategory(e, '')} data-no-instant>all {categoryLinks} Create a Listing + Manage Listings
this.handleQuery(e)} /> diff --git a/app/javascript/packs/listingDashboard.jsx b/app/javascript/packs/listingDashboard.jsx new file mode 100644 index 000000000..26f2fdab6 --- /dev/null +++ b/app/javascript/packs/listingDashboard.jsx @@ -0,0 +1,15 @@ +import { h, render } from 'preact'; +import { ListingDashboard } from '../listings/listingDashboard'; + +function loadElement() { + const root = document.getElementById('classifieds-listings-dashboard'); + if (root) { + render(, root, root.firstElementChild); + } +} + +window.InstantClick.on('change', () => { + loadElement(); +}); + +loadElement(); diff --git a/app/views/classified_listings/_form.html.erb b/app/views/classified_listings/_form.html.erb index bebe67773..8c2b2c45f 100644 --- a/app/views/classified_listings/_form.html.erb +++ b/app/views/classified_listings/_form.html.erb @@ -1,6 +1,6 @@ <%= form_with(model: classified_listing, local: true) do |form| %>
- < go back to listings + < return to listings dashboard

Create a Community Listing

diff --git a/app/views/classified_listings/dashboard.html.erb b/app/views/classified_listings/dashboard.html.erb new file mode 100644 index 000000000..528b7a3f6 --- /dev/null +++ b/app/views/classified_listings/dashboard.html.erb @@ -0,0 +1,17 @@ + +
+
+<%= javascript_pack_tag "listingDashboard", defer: true %> diff --git a/config/routes.rb b/config/routes.rb index c2414bc13..713b6cb07 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -139,7 +139,7 @@ Rails.application.routes.draw do resources :tag_adjustments, only: [:create] resources :rating_votes, only: [:create] resources :page_views, only: %i[create update] - resources :classified_listings, path: :listings, only: %i[index new create edit update delete] + resources :classified_listings, path: :listings, only: %i[index new create edit update delete dashboard] resources :credits, only: %i[index new create] resources :buffer_updates, only: [:create] resources :reading_list_items, only: [:update] @@ -148,6 +148,7 @@ Rails.application.routes.draw do get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id" get "/credits/purchase" => "credits#new" + get "/listings/dashboard" => "classified_listings#dashboard" get "/listings/:category" => "classified_listings#index" get "/listings/:category/:slug" => "classified_listings#index" get "/listings/:category/:slug/:view" => "classified_listings#index",