7
\$\begingroup\$

I'm a beginner developer and I'm building a calendar for smartphones. I'm using HTML, CSS and JS. I'm not entirely done with the project yet, however, I have the feeling that I'm making a messy code. I'll be posting the link to the git + the code in here, so you can better read it/clone, and help me with design patterns, etc...

P.S.: The project was developed using the IPhoneX resolution (375 x 812). It is not responsive yet, so I would recommend running the code using the resolution above.

Git link: https://github.com/LucasBiazi/Mobile_Calendar_HTML_CSS_JS

HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Calendar</title>
    <script src="../script/script.js"></script>
    <link rel="stylesheet" href="../css/style.css" />
  </head>
  <!-- As soon as the page loads, fulfills the table. -->
  <body onload="load_DB(new Date().getFullYear(), new Date().getMonth())">
    <div class="main">
      <div class="title">
        <span class="year_title" id="year_title"></span>
        <span class="month_title" id="month_title"></span>
      </div>
      <div class="calendar">
        <div id="month_days" class="month_days">
          <table id="days">
            <tr>
              <th style="color: lightsalmon">Sun</th>
              <th class="even">Mon</th>
              <th>Tue</th>
              <th class="even">Wed</th>
              <th>Thu</th>
              <th class="even">Fri</th>
              <th style="color: lightsalmon">Sat</th>
            </tr>
          </table>
        </div>
        <!-- Future implementations. -->
        <div class="data">
          <div class="data_content">
            <p style="color: black; font-size: 20px">No content.</p>
          </div>
        </div>
      </div>
      <div class="buttons">
        <!-- Reteats a month. -->
        <button
          style="
            animation: display_button_back 1s ease;
            position: relative;
          "
          onclick="retreat_month(parseInt(document.getElementById('year_title').textContent), 
        identify_month(document.getElementById('month_title').textContent))"
        >
          <img
            src="../../media/left-arrow-line-symbol.svg"
            style="width: 35px; height: 35px"
            alt="back"
          />
        </button>
        <!-- Shows current month. -->
        <button
          style="animation: display_opacity_zoom 1s ease-out;"
          onclick="advance_month(new Date().getFullYear(), new Date().getMonth() - 1)"
        >
          T
        </button>
        <!-- Advances a month. -->
        <button
          style="
            animation: display_button_next 1s ease;
            position: relative;
          "
          onclick="advance_month(parseInt(document.getElementById('year_title').textContent), 
          identify_month(document.getElementById('month_title').textContent))"
        >
          <img
            src="../../media/right-arrow-angle.svg"
            style="width: 35px; height: 35px"
            alt="next"
          />
        </button>
      </div>
    </div>
  </body>
</html>

CSS:

* {
  padding: 0px;
  margin: 0px;
  font-family: Verdana, Geneva, Tahoma, sans-serif;
  font-weight: lighter;
}

body {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-image: url(../../media/grass.jpg);
  /* Blurring the background. Applies behind the element... */
  backdrop-filter: blur(9px);
  background-size: cover;
}

@keyframes display_data {
  0% {
    transform: scale3d(0.25, 0, 1);
  }
  25% {
    transform: scale3d(0.5, 0, 1);
  }
  50% {
    transform: scale3d(0.1, 0, 1);
  }
  75% {
    transform: scale3d(0.1, 1.2, 1);
  }
  100% {
    transform: scale3d(1, 1, 1);
  }
}
@keyframes display_month_days {
  from {
    opacity: 0%;
  }
  to {
    opacity: 100%;
  }
}
@keyframes display_button_back {
  0% {
    right: 25px;
    transform: scale3d(0.75, 0.75, 1);
  }
  100% {
    right: 0px;
    transform: scale3d(1, 1, 1);
  }
}
@keyframes display_button_next {
  0% {
    left: 25px;
    transform: scale3d(0.75, 0.75, 1);
  }
  100% {
    left: 0px;
    transform: scale3d(1, 1, 1);
  }
}
@keyframes display_opacity_zoom {
  from {
    opacity: 0%;
    transform: scale3d(0.5, 0.5, 1);
  }
  to {
    opacity: 100%;
    transform: scale3d(1, 1, 1);
  }
}
.main {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  color: white;
  background-color: rgba(0, 0, 0, 0.65);
}

.title {
  margin-top: 7%;
  height: 80px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
  animation: display_opacity_zoom 1s ease-out;
}

.year_title {
  margin-left: 5px;
  font-size: 40px;
  letter-spacing: 5px;
  color: lightsalmon;
  text-align: center;
}

.month_title {
  margin-left: 15px;
  font-size: 25px;
  letter-spacing: 15px;
  text-align: center;
}

.calendar {
  height: 75%;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
}

.month_days {
  margin-top: 10px;
  width: 100%;
  height: 50%;
  animation: display_month_days 1s ease-in-out;
}

table {
  margin-top: 20px;
  width: 100%;
  font-size: 22px;
}

tr,
th,
td {
  background-color: none;
}

th {
  width: 14%;
  text-align: center;
  color: white;
}

td {
  width: 2.38em;
  height: 2.38em;
  color: white;
  text-align: center;
  border-radius: 50%;
}

td:hover {
  background-color: black;
}

.data {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 95%;
  height: 30%;
  background-color: rgba(255, 255, 255, 0.9);
  border: none;
  border-radius: 5px;
  animation: display_data 0.8s ease;
}

.data_content {
  width: 95%;
  height: 95%;
}

.other_month {
  background: none;
  color: rgba(175, 175, 175, 0.45);
}

.buttons {
  width: 100vw;
  height: 70px;
  display: flex;
  justify-content: space-around;
  align-items: flex-start;
}

button {
  width: 60px;
  height: 60px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: none;
  border: none;
  font-size: 35px;
  font-weight: 400;
  color: white;
}

JS:

// Returns the day of the week in which the month starts.
starting_day = (year, month) => new Date(year, month, 1).getDay();

// Returns the amount of days in a month.
total_month_days = (year, month) => new Date(year, month + 1, 0).getDate();

// Set the background color of the cell that contains today's day.
function is_today(months, cell) {
  if (
    new Date().getFullYear() ==
      parseInt(document.getElementById("year_title").textContent) &&
    months[new Date().getMonth()].month ==
      document.getElementById("month_title").textContent &&
    cell.textContent == new Date().getDate()
  ) {
    cell.style.background = "rgba(255, 160, 122, 0.8)";
  }
}

// Changes color if it is a weekend.
color_weekend = (let_value, cell) => {
  if (let_value == 6 || let_value == 0) cell.style.color = "lightsalmon";
};

// Populates the DB.
function load_DB(year, month) {
  let counter = 1;
  const table = document.getElementById("days");
  const date_object = new Date(year, month);

  // Array containing months names, starting_day()_+ total_month_days().
  const months = [
    {
      // Month 0
      month: "January",
      first_day: starting_day(date_object.getFullYear(), 0),
      days: Array(total_month_days(date_object.getFullYear(), 0)),
    },
    {
      // Month 1
      month: "February",
      first_day: starting_day(date_object.getFullYear(), 1),
      days: Array(total_month_days(date_object.getFullYear(), 1)),
    },
    {
      // Month 2
      month: "March",
      first_day: starting_day(date_object.getFullYear(), 2),
      days: Array(total_month_days(date_object.getFullYear(), 2)),
    },
    {
      // Month 3
      month: "April",
      first_day: starting_day(date_object.getFullYear(), 3),
      days: Array(total_month_days(date_object.getFullYear(), 3)),
    },
    {
      // Month 4
      month: "May",
      first_day: starting_day(date_object.getFullYear(), 4),
      days: Array(total_month_days(date_object.getFullYear(), 4)),
    },
    {
      // Month 5
      month: "June",
      first_day: starting_day(date_object.getFullYear(), 5),
      days: Array(total_month_days(date_object.getFullYear(), 5)),
    },
    {
      // Month 6
      month: "July",
      first_day: starting_day(date_object.getFullYear(), 6),
      days: Array(total_month_days(date_object.getFullYear(), 6)),
    },
    {
      // Month 7
      month: "August",
      first_day: starting_day(date_object.getFullYear(), 7),
      days: Array(total_month_days(date_object.getFullYear(), 7)),
    },
    {
      // Month 8
      month: "September",
      first_day: starting_day(date_object.getFullYear(), 8),
      days: Array(total_month_days(date_object.getFullYear(), 8)),
    },
    {
      // Month 9
      month: "October",
      first_day: starting_day(date_object.getFullYear(), 9),
      days: Array(total_month_days(date_object.getFullYear(), 9)),
    },
    {
      // Month 10
      month: "November",
      first_day: starting_day(date_object.getFullYear(), 10),
      days: Array(total_month_days(date_object.getFullYear(), 10)),
    },
    {
      // Month 11
      month: "December",
      first_day: starting_day(date_object.getFullYear(), 11),
      days: Array(total_month_days(date_object.getFullYear(), 11)),
    },
  ];

  // Prints the name of the current month and year.
  document.getElementById("year_title").textContent = date_object.getFullYear();
  document.getElementById("month_title").textContent =
    months[date_object.getMonth()].month;

  // Month days that will appear in the first row.
  const number_of_cells_in_the_first_row =
    7 - months[date_object.getMonth()].first_day;

  // Month days that will appear after the first row.
  let normal_days = number_of_cells_in_the_first_row + 1;

  // Creates + populates the 5 last rows.
  for (let r = 0; r < 5; r++) {
    let row = table.insertRow(r + 1);
    let cell;
    // Creates + populates 7 cells in each row.
    for (let x = 0; x < 7; x++) {
      cell = row.insertCell(x);
      cell.textContent = normal_days;
      is_today(months, cell);
      color_weekend(x, cell);
      normal_days++;
      // Filling the rest of the table with the days of the next month(gray days).
      if (normal_days > months[date_object.getMonth()].days.length + 1) {
        // Next month days are always lowlighted.
        if (x == 6 || x == 0) cell.style.color = "rgba(175, 175, 175, 0.45)";
        cell.textContent = counter++;
        cell.className = "other_month";
      }
    }
  }

  // Creates + populates the 1 row.
  for (let i = 0; i < 1; i++) {
    let row = table.insertRow(i + 1);
    let cell;
    let number_of_blank_cells = 7 - number_of_cells_in_the_first_row;
    // Populates it.
    for (let y = 0; y < number_of_cells_in_the_first_row; y++) {
      cell = row.insertCell(0);
      cell.textContent = number_of_cells_in_the_first_row - y;
      is_today(months, cell);
      color_weekend(y, cell);
    }
    // Filling the rest of the table (next month days).
    for (let w = 0; w < number_of_blank_cells; w++) {
      cell = row.insertCell(0);
      if (months[date_object.getMonth() - 1]) {
        cell.textContent = months[date_object.getMonth() - 1].days.length--;
        cell.className = "other_month";
      } else {
        cell.textContent = months[date_object.getMonth() + 11].days.length--;
        cell.className = "other_month";
      }
    }
  }
}

// Converts the month name to the month number (January = 0).
function identify_month(string) {
  const all_months = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];
  let i = 0;
  while (string != all_months[i]) {
    i++;
  }
  return i;
}

// Advancecs a month.
function advance_month(year, month) {
  // Cleaning table.
  const table = document.getElementById("days");
  for (let i = 0; i < 6; i++) {
    table.deleteRow(1);
  }
  // Add new data.
  month++;
  load_DB(year, month);
}

// Retreats a month.
function retreat_month(year, month) {
  // Cleaning table.
  const table = document.getElementById("days");
  for (let i = 0; i < 6; i++) {
    table.deleteRow(1);
  }
  month--; // Add new data.
  load_DB(year, month);
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Welcome to the Code Review Site, it is best to wait until the code is finished, but we can help with this. Remember not to change any part of the question after you have an answer. \$\endgroup\$ – pacmaninbw Oct 16 '20 at 19:43
6
\$\begingroup\$

Welcome to Code Review. The CSS looks quite good. I haven’t seen too many style sheets with color lightsalmon 😃. Below are some suggestions.

Cache DOM references

bridge toll

”...DOM access is actually pretty costly - I think of it like if I have a bridge - like two pieces of land with a toll bridge, and the JavaScript engine is on one side, and the DOM is on the other, and every time I want to access the DOM from the JavaScript engine, I have to pay that toll”
    - John Hrvatin, Microsoft, MIX09, in this talk Building High Performance Web Applications and Sites at 29:38, also cited in the O'Reilly Javascript book by Nicholas C Zakas Pg 36, as well as mentioned in this post

Bearing in mind that was stated more than 10 years ago and browsers likely have come along way since then, it is still wise to be aware of this. Store references to DOM elements - e.g. document.getElementById("year_title") in variables- this could happen in a callback to the DOMContentLoaded event.

let titleEl;
window.addEventListener('DOMContentLoaded', (event) => {
    titleEl = document.getElementById("year_title");
});

As Joshua mentioned, event handlers in HTML are frowned upon by today’s standards, and that callback would be an appropriate place to have the code currently in the onload attribute of the body element.

month names

As Joshua already mentioned the code for the months is quite repetitive. Instead of storing the month names Date.prototype.toLocaleDateString() could be used to generate the month names dynamically, possibly in a language specified by the user and/or navigator.language if it is available.

More readable function name

// Set the background color of the cell that contains today's day.
function is_today(months, cell) {

Judging by the name I would guess the function would return a Boolean that indicates if some value is today, though given the comment and the implementation that is not the case. A more appropriate name might be style_todays_date_cell.

Strict equality

It is a good habit to use strict comparison operators - i.e. === and !== when possible - e.g. for this comparison within load_DB():

if (x == 6 || x == 0) cell.style.color = "rgba(175, 175, 175, 0.45)";

Pre-fix increment

These lines:

month++;
load_DB(year, month);

Can be combined when the value of month is incremented before the call using the prefix increment operator:

 load_DB(year, ++month);

redundant inline styles -> CSS

The images within the buttons have inline styles- i.e.

 style="width: 35px; height: 35px"

That can be moved to the stylesheet:

button img {
    width: 35px;
    height: 35px;
}

Inline styles for table headers

The first and last cells have inline styles to override the default th rule:

<th style="color: lightsalmon">Sun</th>

That can be moved to the stylesheet as well using more specific selectors- e.g. pseudo-selectors :first-child and :last-child:

th:first-child,
th:last-child {
    color: lightsalmon;
}
          

Alternatives include :first-of-type and :last-of-type That technique could also be used to move the line above out of the JavaScript and into the style sheet using td instead of tr:

if (x == 6 || x == 0) cell.style.color = "rgba(175, 175, 175, 0.45)";

P.S.: The answer was composed using the IPhone VII+

\$\endgroup\$
6
\$\begingroup\$

Variables

Always use a variable declaration when first declaring your variable. If you don't your variable will be put into the global scope, which is almost never what you want. It also makes it more clear that you don't want to re-assign to an already existing global variable.

So for starting_day, for example, put a var, let, or const in front of it.

HTML Attributes

Instead of using attributes like onclick or onload to do event handling (these are also known as inline handlers), it is generally considered best practice to handle these events in your JavaScript itself. It helps to separate your HTML/JavaScript.

For example, to replace the onload attribute, you could instead use the following JS function:

function ready(func) {
  if (document.readyState !== 'loading') {
    func();
  } else {
    document.addEventListener('DOMContentLoaded', func);
  }
}

You could declare a function called main for example, where you would put any code that you want to run when the DOM is ready to be worked with, and then call ready(main);.

To replace the onclick attributes, you should add some CSS classes/ids to them to be used as selectors in the JavaScript code. You would then select them using something like const backButton = document.querySelector('calendarBack');. Then, you can attach an event listener to that element like so:

backButton.addEventListener("click", () => {
   // The code you want to run when the button is clicked goes here
});

Code Refactorings

months

The months variable declaration is rather repetitive and there is only one thing changing for each month, the 'index' of it. This makes it a perfect candidate to roll into a loop/array map.

I would do it like this using the map function:

// This would ideally not be in load_DB but be a top level declaration, since you could then reference this again in the identify_month function instead of rewriting the months
const monthNames = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

// Saving this in a variable for brevity and since it doesn't change
const fullYear = date_object.getFullYear();

const months = monthNames.map((monthName, index) => {
  return {
    month: monthName,
    first_day: starting_day(fullYear, index),
    days: Array(total_month_days(fullYear, index)),
  };
});

One thing that I'm not sure of is the days property and why it is an array to begin with. The only places it is being referenced in code is to check its length. Is this part of the unfinished code that you mentioned? If it isn't, then I would just replace the line with days: Array(total_month_days(fullYear, index)), instead.

identify_month

The original implementation of this function should work, although it is a long way of saying "get the index of the month from the monthNames array".

Here is how I would rewrite it:

// Converts the month name to the month number (January = 0).
function identify_month(string) {
  return monthNames.indexOf(string);
}

Current month code

The original code is rather hard to read because of the repeated lookups of the month object found in the months array. Place them into a variable to improve readability and remove unneeded additional array accesses.

const currentMonth = date_object.getMonth();

const currentMonthObj = months[currentMonth];

// Prints the name of the current month and year.
document.getElementById("year_title").textContent = fullYear;
document.getElementById("month_title").textContent = currentMonthObj.month;

// Month days that will appear in the first row.
const number_of_cells_in_the_first_row = 7 - currentMonthObj.first_day;

I will try to come back and add some more thoughts later :)

\$\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.