4
\$\begingroup\$

I'm looking for specific feedback on a major design change that I plan on making to my personal website (https://aleksandrhovhannisyan.github.io/).

Currently, the Projects section contains hardcoded HTML. With my current approach, each time I want to add a new project, I have to copy-paste an existing project div and customizing the information. Even worse, some of the information, like the stargazer count, must be updated with new commits each time that information changes. There's also the issue of adding icons to each project, and they're not always uniform in dimensions (noob alert).

My alternative approach, which I've been working on for a couple days now, is to pull data for my repositories using the GitHub API and AJAX. Don't worry—I'm not authenticating, so no client credentials have to be exposed.

The code (HTML, CSS, JS) is below. Please note that the JavaScript originally contains other functionality as well, like for the light/dark mode switch and the navbar hamburger icon, and so on. But those functions are not relevant for this code review, so I've omitted them to make things easier.

Also, I exceeded my character limit for this post, so I stripped most of the HTML that isn't relevant.

Below are some specific questions I'm hoping people could answer. However, I am more than open to comments on anything else you notice!

  1. Code style/cleanliness: If it isn't obvious, I'm sort of new to JavaScript and working with APIs. This "overhaul" is my way of getting my feet wet and learning a bit more. Is there anything that makes the JavaScript difficult to read or understand? Does the fact that I have so many functions make it more difficult to keep track of how data is passed around?

  2. The repos map: Is it okay that I have the global repos map up at the top? Is my approach here okay/understandable? What about the get convenience function I defined: is there a better approach? Again, any feedback is appreciated!

  3. Be honest: Which version of the site's Projects section do you prefer—the one you see here or the original linked at the top of this post? And for what reason(s)?

Thank you in advance! Here's the code:

var repos = new Map();

setupRepos();
requestRepoData();


/** Defines all repositories of interest, to be displayed in the Projects section of the page in
 * the precise order that they appear here. These serve as filters for when we scour through all 
 * repositories returned by the GitHub API. Though these are mostly hardcoded, we only have to enter
 * the information within this function; the rest of the script is not hardcoded in that respect.
 * Notable downside: if the name of the repo changes for whatever reason, it will need to be updated here.
 */
function setupRepos() {
    addRepo("Scribe-Text-Editor", "Scribe: Text Editor", ["cpp", "qt5", "qtcreator"]);
    addRepo("EmbodyGame", "Embody: Game", ["csharp", "unity", "ai"]);
    addRepo("aleksandrhovhannisyan.github.io", "Personal Website", ["html5", "css", "javascript"]);
    addRepo("Steering-Behaviors", "Steering Behaviors", ["csharp", "unity", "ai"]);
    addRepo("MIPS-Linked-List", "ASM Linked List", ["mips", "asm", "qtspim"]);
    addRepo('Dimension35', "dim35: Game", ["godot", "networking"]);
}


/** Associates the given official name of a repo with an object representing custom data about that repository.
 * This hashing/association makes it easier to do lookups later on.
 * 
 * @param {string} officialName - The unique name used to identify this repository on GitHub.
 * @param {string} customName - A custom name for the repository, not necessarily the same as its official name.
 * @param {string[]} topics - An array of strings denoting the topics that correspond to this repo.
 */
function addRepo(officialName, customName, topics) {
    // Note 1: We define a custom name here for two reasons: 1) some repo names are quite long, such as my website's,
    // and 2) multi-word repos have hyphens instead of spaces on GitHub, so we'd need to replace those (which would be wasteful)

    // Note 2: We define the topics here instead of parsing them dynamically because GitHub's API returns the topics
    // as a *sorted* array, which means we'll end up displaying undesired tags (since we don't show all of them).
    // This approach gives us more control but sacrifices flexibility, since we have to enter topics manually for repos of interest.
    repos.set(officialName, { "customName" : customName, "topics" : topics, "card" : null });
}


/** Convenience wrapper for accessing the custom data for a particular repo. Uses the given
 * repo's official name (per the GitHub API) as the key into the associated Map.
 * 
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Object} The custom object representing the given repo.
 */
function get(repo) {
    // Notice how the underlying syntax is messy; the wrapper makes it cleaner when used
    return repos.get(repo.name);
}


function requestRepoData() {
    let request = new XMLHttpRequest();
    request.open('GET', 'https://api.github.com/users/AleksandrHovhannisyan/repos', true);
    request.onload = parseRepos;
    request.send();
}


function parseRepos() {
    if (this.status !== 200) return;

    let data = JSON.parse(this.response);

    // Even though we have to loop over all repos to find the ones we want, doing so is arguably
    // much faster (and easier) than making separate API requests for each repo of interest
    // Also note that GitHub has a rate limit of 60 requests/hr for unauthenticated IPs
    for (let repo of data) {
        if (repos.has(repo.name)) {
            // We cache the card here instead of publishing it immediately so we can display
            // the cards in our own order, since the requests are processed out of order (b/c of async)
            get(repo).card = createCardFor(repo);
        }
    }

    publishRepoCards();
}


/** Creates a project card for the given repo. A card consists of a header, description,
 * and footer, as well as an invisible link and hidden content to be displayed when the
 * card is hovered over.
 * 
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} A DOM element representing a project card for the given repo.
 */
function createCardFor(repo) {
    let card = document.createElement('section');
    card.setAttribute('class', 'project');
    card.appendChild(headerFor(repo));
    card.appendChild(descriptionFor(repo));
    card.appendChild(footerFor(repo));
    card.appendChild(anchorFor(repo));
    card.appendChild(createHoverContent());
    return card;
}


/**
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} A header for the given repo, consisting of three key pieces:
 * the repo icon, the repo name, and the repo's rating (stargazers).
 */
function headerFor(repo) {
    var header = document.createElement('header');

    var icon = document.createElement('span');
    icon.setAttribute('class', 'project-icon');
    // The emoji part of the description on GitHub
    icon.textContent = repo.description.substring(0, 3);
    
    var h4 = document.createElement('h4');
    h4.appendChild(icon);
    h4.appendChild(nameLabelFor(repo));
    
    header.appendChild(h4);
    header.appendChild(stargazerLabelFor(repo));
    return header;
}


/**
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} A label for the name of the given repo.
 */
function nameLabelFor(repo) {
    var projectName = document.createElement('span');
    projectName.textContent = get(repo).customName;
    return projectName;
}


/**
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} A label showing the number of stargazers for the given repo.
 */
function stargazerLabelFor(repo) {
    var projectRating = document.createElement('span');
    
    var starIcon = document.createElement('i');
    starIcon.setAttribute('class', 'fas fa-star filled');
    
    var starCount = document.createElement('span');
    starCount.textContent = ' ' + repo.stargazers_count;
    
    projectRating.setAttribute('class', 'project-rating');
    projectRating.appendChild(starIcon);
    projectRating.appendChild(starCount);
    
    return projectRating;
}


/**
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} An element containing the description of the given repo.
 */
function descriptionFor(repo) {
    var description = document.createElement('p');
    description.setAttribute('class', 'description');
    // Non-emoji part of the description on GitHub
    description.textContent = repo.description.substring(3);
    return description;
}


/**
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} A footer for the name of the given repo, consisting of at most
 * three paragraphs denoting the topics associated with that repo.
 */
function footerFor(repo) {
    var footer = document.createElement('footer');
    footer.setAttribute('class', 'topics');

    for(let topic of get(repo).topics) {
        let p = document.createElement('p');
        p.textContent = topic;
        footer.appendChild(p);
    }

    return footer;
}


/**
 * @param {Object} repo - The JSON-parsed object containing a repository's data.
 * @returns {Element} An anchor element whose href is set to the given repo's "real" URL.
 */
function anchorFor(repo) {
    var anchor = document.createElement('a');
    anchor.setAttribute('class', 'container-link');
    anchor.setAttribute('href', repo.html_url);
    anchor.setAttribute('target', '_blank');
    return anchor;
}


function createHoverContent() {
    var hoverContent = document.createElement('div');
    hoverContent.setAttribute('class', 'hover-content');
    
    var boldText = document.createElement('strong');
    boldText.textContent = 'View on GitHub';

    var externalLinkIcon = document.createElement('i');
    externalLinkIcon.setAttribute('class', 'fas fa-external-link-alt');
    
    hoverContent.appendChild(boldText);
    hoverContent.appendChild(externalLinkIcon);
    return hoverContent;
}


function publishRepoCards() {
    const projects = document.getElementById('projects');
    const placeholder = document.getElementById('project-placeholder');

    for (let repo of repos.values()) {
        projects.insertBefore(repo.card, placeholder);
    }
}
/* ============================================

   General top-level styling

   ============================================
*/

* {
    box-sizing: border-box;
}

:root {
    --main-bg-color: white;

    --nav-bg-color: rgb(44, 44, 44);
    --nav-text-color: rgb(179, 177, 177);
    --nav-min-height: 50px;
    --topic-label-bg-color: #e7e7e7;

    --hr-color: rgba(143, 142, 142, 0.2);

    --text-color-normal: black;
    --text-color-emphasis: black;
    --link-color: rgb(39, 83, 133);

    --button-bg-color: rgb(39, 83, 133);
    --button-bg-hover-color: rgb(83, 129, 182);
    --button-text-color: white;
    --button-text-hover-color: white;

    --skill-hover-bg-color: whitesmoke;
    
    --project-card-bg-color: rgb(253, 253, 253);
    --project-card-shadow: 0px 0px 4px 2px rgba(50, 50, 50, 0.4);
    --project-card-shadow-hover: 0px 1px 6px 2px rgba(10, 10, 10, 0.6);
    --project-card-margin: 30px;
    
    --form-bg-color: rgb(255, 255, 255);
    --form-input-margins: 10px;
    --form-max-width: 475px;

    --page-center-percentage: 80%;
    --global-transition-duration: 0.5s;
    --institution-info-border-width: 3px;
}

.night {
    --main-bg-color: rgb(44, 44, 44);
    --nav-bg-color: rgb(10, 10, 10);
    --topic-label-bg-color: #222222;
    
    --hr-color: rgba(255, 255, 255, 0.2);

    --text-color-normal: rgb(179, 177, 177);
    --text-color-emphasis: rgb(202, 202, 202);
    --link-color: rgb(202, 183, 143);
    
    --button-bg-color: rgb(90, 90, 66);
    --button-bg-hover-color: rgb(141, 141, 114);
    --button-text-color: var(--text-color-emphasis);
    --button-text-hover-color: rgb(24, 24, 24);
 
    --skill-hover-bg-color: rgb(66, 66, 66);
    --project-card-bg-color: rgb(54, 54, 54);
    /* The shadows need to be a bit more prominent so they contrast well in dark mode,
    hence the larger values for blur and spread */
    --project-card-shadow: 0 2px 6px 4px rgba(31, 31, 31, 0.9);
    --project-card-shadow-hover: 0px 2px 10px 5px rgba(10, 10, 10, 0.6);
    --form-bg-color: var(--skill-hover-bg-color);
}

#intro {
    margin-bottom: 40px;
}

#about-me, #projects, #skills, #education, #contact {    
    /* So the fixed navbar doesn't cover up any content we scroll to */
    margin-top: calc((var(--nav-min-height) + 20px) * -1);
    padding-top: calc(var(--nav-min-height) + 20px);
}

#about-me, #projects, #skills, #education {
    margin-bottom: 120px;
}

body {
    font-family: Nunito, sans-serif;
    color: var(--text-color-normal);
    background-color: var(--main-bg-color);
    transition: background-color var(--global-transition-duration);
    
    width: var(--page-center-percentage);
    margin-left: auto;
    margin-right: auto;
}

i, h1, h2, h4, strong, em {
    color: var(--text-color-emphasis);
}

.institution-info h4 {
    margin-left: 10px;
    font-weight: normal;
    color: var(--text-color-normal);
}

h1 {
    font-size: 2em;
    margin-block-start: 0.67em;
    margin-block-end: 0.67em;
}

h1, h2 {
    margin-top: 0;
}

a {
    color: var(--link-color);
}

p {
    color: var(--text-color-normal);
}

/* Links an entire parent container, but the parent must be set to relative position */
.container-link {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    text-decoration: none;
    z-index: 1;
}


/* ============================================

   Buttons, collapsibles, etc.

   ============================================
*/

/* Note: this is an anchor with a class of button */
.button {
    width: 100%;
    height: 40px;
    line-height: 40px;
    text-align: center;
    display: block;
    
    margin-bottom: 10px;
    margin-right: 15px;
    border-radius: 10px;
    
    font-size: 1em;
    font-weight: bold;
    text-decoration: none;
}

.collapsible {
    font-family: Nunito, sans-serif;
    font-size: 1em;
    display: flex;
    align-items: center;
    border: none;
    outline: none;
    width: 100%;
}

.collapsible span {
    text-align: left;
    padding-left: 10px;
    margin-top: 20px;
    margin-bottom: 20px;
}

.button, .collapsible {
    cursor: pointer;
    border: none;
    background-color: var(--button-bg-color);
    transition: var(--global-transition-duration);
}

.button, .button *, .collapsible * {
    color: var(--button-text-color);
}

.button:hover, .collapsible:hover {
    background-color: var(--button-bg-hover-color);
}

/* To get rid of Firefox's dotted lines when these are clicked */
.button::-moz-focus-inner, .collapsible::-moz-focus-inner {
    border: 0;
}

.button:hover, .button:hover *, .collapsible:hover * {
    color: var(--button-text-hover-color);
}

button:focus {
    outline: none;
}

.fa-angle-right, .fa-angle-down {
    margin-left: 10px;
    margin-right: 20px;
    font-size: 1em;
}

@media only screen and (min-width: 400px) {
    
    .main-buttons {
        display: flex;
    }

    .button {
        max-width: 200px; 
    }
}


/* ============================================

   Navigation (+ night mode nightmode-switch)

   ============================================
*/

#topnav .centered-content {
    width: var(--page-center-percentage);
    margin-left: auto;
    margin-right: auto;
    height: var(--nav-min-height);
    display: flex;
    justify-content: space-between;
    align-items: center;
}

#topnav {
    width: 100%;
    min-height: var(--nav-min-height);
    position: fixed;
    left: 0;
    right: 100%;
    top: 0;
    background-color: var(--nav-bg-color);

    /* This is to ensure that it always appears above everything. */
    z-index: 100;
}

#topnav * {
    color: var(--nav-text-color);
}

.nav-links {
    padding: 0;
    list-style-type: none;
    display: none;
    margin-left: 0;
    margin-right: 0;
}

.nav-links li {
    text-align: center;
    margin: 20px auto;
}

.nav-links a {
    text-decoration: none;
    vertical-align: middle;
    transition: var(--global-transition-duration);
}

#topnav .nav-links a:hover {
    text-decoration: underline;
    color: white;
}

.navbar-hamburger {
    font-size: 1.5em;
}

.nightmode-switch-container, .nightmode-switch-container * {
    display: inline-block;
}

.nightmode-switch {
    width: 40px;
    height: 20px;
    line-height: 15px;
    margin-right: 5px;

    background-color: var(--nav-bg-color);
    border: 3px solid var(--nav-text-color);
    border-radius: 100px;
    cursor: pointer;
    transition: var(--global-transition-duration);
}

.nightmode-switch::before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    line-height: normal;

    margin-left: 2px;
    margin-bottom: 2px;
    width: 12px;
    height: 10px;
    
    background-color: var(--nav-text-color);
    border-radius: 50%;
    transition: var(--global-transition-duration);
}

.night .nightmode-switch::before {
    margin-left: 20px;
}

.nav-links.active {
    display: block;
    background-color: var(--nav-bg-color);
    color: var(--nav-text-color);
    
    /* Make the dropdown take up 100% of the viewport width */
    position: absolute;
    left: 0;
    right: 0;
    top: 20px;
}

@media only screen and (min-width: 820px) {

    /* This is the most important part: shows the links next to each other
    Note: .nav-links.active accounts for an edge case where you open the hamburger
    on a small view and then resize the browser so it's larger.
    */
    .nav-links, .nav-links.active {
        margin: 0;
        position: static;
        display: flex;
        flex-direction: row;
        justify-content: flex-end;
        align-items: center;
        font-size: 1.1em;
    }

    .nav-links li {
        margin: 0;
    }

    .nav-links a {
        margin-left: 40px;
        transition: var(--global-transition-duration);
    }

   .navbar-hamburger {
       display: none;
    }
}

/* ============================================

   Page header (intro, about me)

   ============================================
*/

#page-header {
    margin-top: 100px;
    display: grid;
    column-gap: 50px;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

#main-cta {
    margin-bottom: 30px;
    font-size: 1.1em;
}

/* ============================================

   Projects/portfolio cards 

   ============================================
*/

#projects {
    display: grid;
    column-gap: 50px;
    row-gap: 50px;
    /* Fill up space as it's made available, with each card being a minimum of 250px */
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

/* Don't treat the project header as an item/card, keep it on the top row */
#projects h2 {
    grid-row: 1;
    grid-column: 1 / -1;
}

.project {
    /* To ensure that .project-link (see below) is absolute relative to us and not the page */
    position: relative;
    display: grid;
    grid-template-columns: 1;
    /* Header, description, footer, respectively */
    grid-template-rows: max-content 1fr max-content;
    row-gap: 20px;
}

/* All project cards except the placeholder get a background and box shadow */
.project:not(#project-placeholder) {
    background-color: var(--project-card-bg-color);
    box-shadow: var(--project-card-shadow);
    border-radius: 5px;
    transition: all var(--global-transition-duration);
}

/* Apply margins to all project headers except the placeholder's */
.project:not(#project-placeholder) header {
    margin-top: var(--project-card-margin);
    margin-bottom: 0px;
    margin-left: var(--project-card-margin);
    margin-right: var(--project-card-margin);

    display: grid;
    grid-template-areas: "heading heading rating";
}

.project-icon * {
    width: 24px;
    margin-right: 3px;
    display: inline-block;
    vertical-align: middle;
}

.project h4 {
    margin: 0px;
    align-self: center;
    grid-area: heading;
}

.project-rating {
    font-size: 0.85em;
    justify-self: center;
    align-self: center;
    grid-area: rating;
}

.project .description {
    margin-top: 0px;
    margin-bottom: 0px;
    margin-left: var(--project-card-margin);
    margin-right: var(--project-card-margin);
}

/* Displayed when a user hovers over a project card */
.hover-content {
    font-size: 1.2em;
    /* Again, note that .project has position: relative */
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;

    /* Center the content for the hover layer */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    /* Opacity = 0 means it's hidden by default */
    opacity: 0;
    background-color: var(--skill-hover-bg-color);
    transition: var(--global-transition-duration) ease;
}

/* Make it clearer which card is hovered over */
.project:hover:not(#project-placeholder) {
    box-shadow: var(--project-card-shadow-hover);
}

/* Transition for the hover content changes its opacity */
.project:hover .hover-content {
    cursor: pointer;
    opacity: 0.92;
}

.fa-external-link-alt {
    margin-top: 20px;
}

.project-name {
    color: var(--link-color);
    text-decoration: none;
}

.topics {
    display: flex;
    flex-wrap: wrap;
    grid-row: 3;

    margin-top: 0px;
    margin-bottom: var(--project-card-margin);
    margin-left: var(--project-card-margin);
    margin-right: var(--project-card-margin);
}

.topics p {
    font-size: 0.9em;
    padding: 5px;
    margin-top: 10px;
    margin-bottom: 0px;
    margin-right: 10px;

    border-radius: 2px;
    background-color: var(--topic-label-bg-color);
    box-shadow: 0 0 2px black;
    transition: background-color var(--global-transition-duration);
}

#project-placeholder {
    display: flex;
    flex-direction: column;
    text-align: center;
    justify-content: center;
}

.github-cta {
    display: inline-block;
    
    font-size: 3em;
    margin-top: 20px;

    text-decoration: none;
    color: black;
}

/* ============================================

   Skills (responsive columns)

   ============================================
*/

#skills {
    display: grid;
    column-gap: 50px;
    row-gap: 20px;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}

#skills h2 {
    grid-row: 1;
    grid-column: 1 / -1;
}

.skill-category h4 {
    margin-bottom: 5px;
}

.skill-item {
    margin-top: 10px;
    display: grid;
    column-gap: 10px;
    grid-template-columns: 1fr 1fr;
}

.skill-item:hover {
    background-color: var(--skill-hover-bg-color);
}

.skill-name {
    grid-column: 1;
}

.skill-rating {
    grid-column: 2;
    display: inline;
    text-align: right;
}

.fa-star.filled {
    color: var(--button-bg-color);
}

.fa-star.empty {
    color: var(--nav-text-color);
}

.night .fa-star.filled {
    color: rgb(145, 145, 145);
}

.night .fa-star.empty {
    color: var(--button-bg-color);
}

/* ============================================

   Education (institutions, coursework, etc.)

   ============================================
*/

.institution {
    margin-top: 20px;
}

/* Course and award container */
.institution-info {
    display: grid;
    /* Mobile first: only one column. Changes to two columns on bigger screens. See media query below. */
    grid-template-columns: 1fr;

    /* Will be set to a sufficiently large max-height by corresponding click handler for .collapsible */
    max-height: 0px;
    transition: max-height var(--global-transition-duration);
    overflow: hidden;

    border: solid var(--institution-info-border-width) var(--button-bg-color);
    border-top: none;
}

.institution-info .awards {
    /* Only matters on mobile, where the awards are stacked underneath courses */
    border-top: solid var(--institution-info-border-width) var(--button-bg-color);
}

.institution-info ul {
    padding-right: 10px;
}

.institution-info p {
    padding-left: 10px;
}

/* Line up courses and awards side by side on larger screens */
@media only screen and (min-width: 800px) {

    .institution-info {
        grid-template-rows: 1fr;
        grid-template-columns: auto auto;
    }

    .institution-info .awards {
        /* Now that it's lined up to the right of the courses, there's no need for a top border */
        border-top: none;
        /* But there is for a left border */
        border-left: solid var(--institution-info-border-width) var(--button-bg-color);
    }
}

/* ============================================

   Contact form

   ============================================
*/

#contact {
    display: grid;
    grid-template-areas: "form"
                         "socials";
    grid-template-rows: auto;
    column-gap: 50px;
}

#contact-form {
    grid-area: form;
}

#social-networks {
    grid-area: socials;
}

@media only screen and (min-width: 700px) {
    #contact {
        grid-template-areas: "form form form socials";
    }
}

form {
    margin-bottom: 50px;
    margin-top: 30px;
    max-width: var(--form-max-width);
}

form * {
    color: var(--text-color-normal);
    font-family: Nunito, sans-serif;
    font-size: 1em;
}

form input:not([class="button"]), form textarea {
    height: 30px;
    width: 100%;
    margin-bottom: 15px;
    padding: 10px;
    
    background-color: var(--form-bg-color);
    border: 0px solid;
    box-shadow: 0 0 3px 1px rgb(172, 172, 172);
    border-radius: 3px;
    transition: var(--global-transition-duration);
}

form label {
    margin-bottom: 5px;
    display: block;
}

form input:focus, form textarea:focus {
    outline: none;
    box-shadow: 0 0 5px 2px rgb(155, 155, 155);
}

form textarea {
    max-width: var(--form-max-width);
    min-height: 200px;
    transition: height 0s;
    transition: background-color var(--global-transition-duration);
}

form .button {
    max-width: 100%;
    width: 100%;
    height: 45px;
}

/* Yum, honey */
input.honeypot {
    display: none;
}

/* ============================================

   Social networks

   ============================================
*/

#social-networks {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    grid-template-rows: min-content;
    grid-auto-rows: min-content;
    row-gap: 50px;
    column-gap: 30px;
    margin-bottom: 50px;
}

#social-networks h3 {
    grid-row: 1;
    grid-column: 1 / -1;
}

.social-network {
    /* Position relative because we have an absolutely 
    positioned .container-link as a child */
    position: relative;
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 20px;
}

.social-network:hover {
    cursor: pointer;
    background-color: var(--skill-hover-bg-color);
}

.social-network .fa-stack {
    grid-column: 1;
    display: grid;
}

.fa-stack i {
    align-self: center;
    justify-self: center;
}

/* Whatever icon is being used as the background one */
.fa-stack-2x {
    opacity: 0;
    font-size: 1.5em;
    color: white;
}

.night .fa-stack-2x {
    opacity: 1;
}

.social-network .network-name {
    grid-column: 2;
    align-self: center;
}

#social-networks .fa-linkedin {
    color: #0077B5;
}

#social-networks .fa-github {
    color: black;
}

#social-networks .fa-stack-exchange {
    color: #195398;
}

#social-networks .fa-address-book {
    color: #37A000;
}

#page-footer {
    position: absolute;
    left: 0;
    height: 50px;
    width: 100%;
    background: var(--nav-bg-color);
    color: var(--nav-text-color);

    display: flex;
    justify-content: center;
    align-items: center;
}
<!DOCTYPE html>

<html lang="en">

    <head>
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- Nunito font looks amazing :) -->
        <link href="https://fonts.googleapis.com/css?family=Nunito&display=swap" rel="stylesheet">
        <!-- Font Awesome icons -->
        <script src="https://kit.fontawesome.com/7d7dc6ad85.js"></script>
        <!-- Custom stylesheet -->
        <link rel="stylesheet" href="style.css">
        <!-- Favicon -->
        <link rel="icon" href="favicon.ico" type='image/x-icon'>
        <!-- Preview image (e.g., for LinkedIn or Facebook) -->
        <meta property="og:image" content="https://avatars2.githubusercontent.com/u/19352442?s=400&amp;v=4">
        <title>Aleksandr Hovhannisyan</title>
        <!-- Contact form -->
        <meta name="referrer" content="origin">
    </head>

    <body>

        <nav id="topnav">
            <div class="centered-content">
                <div class="nightmode-switch-container">
                    <div class="nightmode-switch"></div><span>Light mode</span>
                </div>
                <i class="navbar-hamburger fas fa-bars"></i>
                <ul class="nav-links">
                    <li><a href="#about-me">About Me</a></li>
                    <li><a href="#projects">Projects</a></li>
                    <li><a href="#skills">Skills</a></li>
                    <li><a href="#education">Education</a></li>
                    <li><a href="#contact">Contact</a></li>
                </ul>
            </div>
        </nav>

        <article id="content">

           
            <section id="projects">
                <h2>Projects &#128193;</h2>

                <aside id="project-placeholder" class="project">
                    <header>
                        <h4>Want to see more of my work?</h4>
                    </header>
                    <div>
                        <p>Check out my other repos:</p>
                        <a class="github-cta" href="https://github.com/AleksandrHovhannisyan?tab=repositories" target="_blank"><i class="fab fa-github"></i></a>
                    </div>
                </aside>

            </section>

        </article>

        <!-- Custom javascript -->
        <script src="index.js"></script>
    </body>

</html>

\$\endgroup\$
  • \$\begingroup\$ Note to anyone viewing this late: I have since then updated the repo, so the original link is outdated. I also changed my approach slightly, so what you see here for the repo icons will actually end up being text. \$\endgroup\$ – AleksandrH Jul 6 at 18:10
2
+100
\$\begingroup\$

Addressing your questions

  1. Code style/cleanliness Does the fact that I have so many functions make it more difficult to keep track of how data is passed around?

I wouldn't say it makes it more difficult to keep track of how data is passed around...

  1. The repos map: Is it okay that I have the global repos map up at the top? Is my approach here okay/understandable? What about the get convenience function I defined: is there a better approach?

A class could be used to store the repos as a field declaration, however if you wanted it to be private such a feature is currently experimental1, though the revealing module pattern could be used for that.

You could also use a plain-old Javascript Object (i.e. POJO) instead of a set as long as the keys are only going to be string literals but then iteration of the items would not be in the same order they were inserted. Refer to answers to Map vs Object in JavaScript for more information.

Other feedback

Many variable are declared with let but never re-assigned (e.g. let request = new XMLHttpRequest(); in requestRepoData). It is recommended that you default to using const to avoid accidental re-assignment and then use let when you deem it necessary.


I see setAttribute() is used in certain places to add class names to elements - for example:

let card = document.createElement('section');
card.setAttribute('class', 'project');

There is a method: classList.add() that can be used instead:

card.classList.add('project');

For this line in addRepo():

repos.set(officialName, { "customName" : customName, "topics" : topics, "card" : null });

The key names don't need to be in double quotes unless the names contain special characters like hyphens.

repos.set(officialName, { customName : customName, topics : topics, card : null });

Additionally, the shorthand property definition notation could be used to simplify this code to this:

repos.set(officialName, { customName, topics, "card" : null });
\$\endgroup\$
  • \$\begingroup\$ I guess I should've mentioned that I also want the repo cards to appear in a specific order—namely, the order in which their records were inserted into the map. If I use an object instead of a map, wouldn't I lose the ability to traverse the cards in that custom order? \$\endgroup\$ – AleksandrH Jul 3 at 17:50
  • \$\begingroup\$ Yes that ability would be lost. \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Jul 3 at 17:56
  • \$\begingroup\$ Thanks for all the feedback! I didn't ask this originally, but I wonder: Would it be better to hardcode the repo icons (the emoji) alongside all the other info instead of getting those from the GitHub repo description (using substrings)? I currently use icon.textContent = repo.description.substring(0, 3);, which seems a bit hacky/unclean to me. Just curious. \$\endgroup\$ – AleksandrH Jul 3 at 18:24
  • \$\begingroup\$ oh, hmm... neither approach seems very clean... perhaps an ideal scenario would be that the repo would have a meta field to store such information... I considered suggesting a regex to look for emoji chars and conditionally strip them out when displaying the non-emoji description \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Jul 8 at 18:35
  • \$\begingroup\$ Yeah I just went with the hardcoded approach so my repos don't look too noisy. Good enough for me :D \$\endgroup\$ – AleksandrH Jul 8 at 23:32

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.