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!
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?
The
repos
map: Is it okay that I have the globalrepos
map up at the top? Is my approach here okay/understandable? What about theget
convenience function I defined: is there a better approach? Again, any feedback is appreciated!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&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 📁</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>