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
+
+
+
+
+
+`;
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(
+
+ );
+}
+
+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