2
\$\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 yesterday
3
\$\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);

In the CSS, there is a redundant selector

.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 acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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