3
\$\begingroup\$

I have put together this small application that displays a "Users" JSON in an HTML5 table.

I use Bootstrap 3, Axios and Vue.js 2 for this purpose.

Here is the code:

var app = new Vue({
    el: '#app',
    data: {
        users: [],
        loading: true,
        errored: false,
        url: "https://randomuser.me/api/?&results=500&inc=name,location,email,cell,picture",
        search: '',
        page: 1,
        perPage: 25,
        pages: [],
    },
    methods: {
        getUsers() {
            axios
                .get(this.url)
                .then(response => {
                    this.users = response.data.results
                })
                .catch(error => {
                    console.log(error)
                    this.errored = true
                })
                .finally(() => this.loading = false)
        },
        setPages(users) {
            this.pages.length = 0;
            var numberOfPages = Math.ceil(users.length / this.perPage);
            for (var index = 1; index <= numberOfPages; index++) {
                this.pages.push(index);
            }
        },
        paginate(users) {
            var page = this.page;
            var perPage = this.perPage;
            var from = (page * perPage) - perPage;
            var to = (page * perPage);
            return users.slice(from, to);
        },
        scrollToTop() {
            $("html, body").animate({
                scrollTop: 0
            }, 500);
            return false;
        },
    },
    created() {
        this.getUsers();
    },
    watch: {
        displayedUsers() {
            this.setPages(this.searchResults);
        }
    },
    computed: {
        displayedUsers() {
            return this.paginate(this.searchResults);
        },
        searchResults() {
            this.page = 1;
            return this.users.filter((user) => {
                const {
                    first,
                    last
                } = user.name;
                const {
                    email
                } = user;
                const {
                    city
                } = user.location;
                const lowerCaseSearch = this.search.toLowerCase()
                return `${first} ${last}`.toLowerCase().match(lowerCaseSearch) ||
                    email.toLowerCase().match(lowerCaseSearch) ||
                    city.toLowerCase().match(lowerCaseSearch);
            });
        }
    },
    filters: {
        lowercase(value) {
            return value.toLowerCase();
        },
        capitalize(value) {
            return value.charAt(0).toUpperCase() + value.slice(1);
        },
        titlecase(value) {
            return value.toLowerCase().replace(/(?:^|[\s-/])\w/g, function(match) {
                return match.toUpperCase();
            })
        }
    }
});
.table-container {
  margin: 10px;
}
.table-container .panel-heading {
  font-weight: bold;
}
.table-container .panel-heading {
  display: flex;
  align-items: center;
}
.table-container .panel-heading h2 {
  margin: 0 auto 0 0;
  font-size: 18px;
  font-weight: bold;
}
.table-container .panel-heading .searchbox {
  margin-left: 10px;
}
.table-container .panel-body {
  padding: 0;
}
.table-container table {
  margin-bottom: 0;
  border: none;
}
.table-container table tr:last-child td {
  border-bottom: none;
}
.table-container table tr th {
  font-weight: bold;
}
.table-container table tr th:first-child, .table-container table tr td:first-child {
  border-left: none;
}
.table-container table tr th:last-child, .table-container table tr td:last-child {
  border-right: none;
}
.table-container table tr td {
  padding: 2px 8px !important;
  vertical-align: middle;
}
.table-container table tr td .picture {
  padding-right: 10px;
}
.table-container table tr td img {
  max-height: 30px;
  width: auto;
  border: 1px solid #c7c7c7;
}

.pagination {
  margin-top: 5px;
}
.pagination li a:focus, .pagination li a:hover {
  background: inherit;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>

<div id="app" class="container">
  <div class="panel panel-default table-container">
    <div class="panel-heading clearfix">
      <h2 class="pull-left">Users</h2>
      <div class="searchbox">
        <input type="text" v-model="search" class="form-control" placeholder="Search...">
      </div>
    </div>
    <div class="panel-body">
      <div class="table-responsive">
        <table class="table table-striped table-bordered" id="dataTable">
          <thead>
            <tr>
              <th class="text-right">#</th>
              <th>Name</th>
              <th>Email</th>
              <th>City</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(user, index) in displayedUsers">
              <td class="text-right">{{perPage * (page - 1) + index + 1}}</td>
              <td>
                <span class="picture">
                  <img :src="user.picture.thumbnail" :alt="user.name.first + ' ' + user.name.last" class="img-circle">
                </span>
                <span>{{user.name.first | capitalize}} {{user.name.last | capitalize}}</span>
              </td>
              <td><a :href="'mailto:' + user.email | lowercase">{{user.email | lowercase}}</a></td>
              <td>{{user.location.city | titlecase}}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
  <nav class="text-center" aria-label="Page navigation">
    <ul class="pagination pagination-sm">
      <li @click="scrollToTop">
        <a href="#" @click="page = 1;" aria-label="First">
          <span aria-hidden="true">&laquo;</span>
        </a>
      </li>
      <li @click="scrollToTop">
        <a href="#" v-if="page != 1" @click="page--;" aria-label="Previous">
          <span aria-hidden="true">&lsaquo;</span>
        </a>
      </li>
      <li v-for="pageNumber in pages.slice(page-1, page+4)" :class="{'active': page === pageNumber}" @click="scrollToTop"><a href="#" @click="page = pageNumber;">{{pageNumber}}</a></li>
      <li @click="scrollToTop">
        <a href="#" @click="page++" v-if="page < pages.length" aria-label="Next">
          <span aria-hidden="true">&rsaquo;</span>
        </a>
      </li>
      <li @click="scrollToTop">
        <a href="#" @click="page = pages.length;" aria-label="Last">
          <span aria-hidden="true">&raquo;</span>
        </a>
      </li>
    </ul>
  </nav>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

It works, but I am certain there is room for improvement.

\$\endgroup\$
  • 2
    \$\begingroup\$ Please do not edit the code after people have started posting answers. \$\endgroup\$ – Donald.McLean Jan 15 at 15:43
4
\$\begingroup\$

Overall the code looks fine. There aren't any drastic changes I would advise - just mostly a few simplifications. As far as the design goes it looks good, though it may be wise to add a spinner icon or some other indication while the data is loading, lest the user think nothing is happening. And it doesn't appear that anything happens when errored is set to true. Perhaps the UI should notify the user that there was a problem fetching the data.

The URL doesn't really change so it doesn't need to be in the data object. I would move it out to a constant.

const API_URL = "https://randomuser.me/api/?&results=500&inc=name,location,email,cell,picture";

I understand what the following block in the filter function of searchResults() is doing:

            const {
                first,
                last
            } = user.name;
            const {
                email
            } = user;
            const {
                city
            } = user.location;

But is it really necessary to define all of those things instead of just using the properties, as in below? I could maybe see a point for the nested properties but not so much for user.email...

return `${user.name.first} ${user.name.last}`.toLowerCase().match(lowerCaseSearch) ||
    user.email.toLowerCase().match(lowerCaseSearch) ||
    user.location.city.toLowerCase().match(lowerCaseSearch);

The following CSS rulesets could be combined:

.table-container .panel-heading {
  font-weight: bold;
}
.table-container .panel-heading {
  display: flex;
  align-items: center;
}

The font-weight specification could be moved into the lower declaration block, unless you wanted to pull the bold specification out of the heading ruleset (i.e. .table-container .panel-heading h2) and combine it with the lone bold rule.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.