Aleksandr Hovhannisyan
- 537
- 1
- 3
- 12
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');
const numTopicsToShow = 3;
for(let topic of get(repo).topics) {
let p = document.createElement('p');
p.textContent = topic;
footer.appendChild(p);
if (footer.childElementCount === numTopicsToShow) break;
}
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>
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');
const numTopicsToShow = 3;
for(let topic of get(repo).topics) {
let p = document.createElement('p');
p.textContent = topic;
footer.appendChild(p);
if (footer.childElementCount === numTopicsToShow) break;
}
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>
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>
AJAX and JavaScript: Pulling data from the GitHub API for user repositories
Loading
Loading
lang-js