My portfolio portrays some of my projects and articles.
I created this project so companies and hiring managers can easily understand my experience and skills.
I used Astro 4, Sass, TypeScript and Zod on this project.
Astro is great for content websites, and I used many of its features:
- Static Site Generation
- Layouts
- Routing
- Font and image optimization
- Content collections
- CSS Scoping
There are many things for which we do not need Sass anymore, since CSS has incorporated many of them. However, I still used Sass for some of its unique features, like:
- @mixins
- @extends
TypeScript and Zod helped me a lot to catch bugs ahead of time, and to ensure my content schema would work.
-
Responsiveness: tested with default font-size of 32px and 500% of zoom;
-
Accessibility: tested with keyboard navigation and screen reader;
-
View Transition API;
-
Menu structure: I used OOP to structure the menu behavior. The code became much easier to update and read, and reducing the ammount of parameter I had to pass to each function. Some of the functionalities in the menu:
-
It opens and closes when the button is clicked;
-
It triggers a focus trap to make keyboard navigation easier;
-
It updates WAI-aria settings;
-
It can be closed with escape on the keyboard or by clicking outside of the menu;
-
On screen resize, all settings are updated
The code was like this:
menuEngine
import {
toggleMenu,
updateOnLoadAndResize,
focusableNotLinks,
closeMenu,
} from "./toggleMenu.ts";
function startMenu() {
const buttonParent = document.querySelector(".burger-button");
const button = buttonParent?.querySelector(".burger-button button");
const menu = document.querySelector(".navigation-items");
const links = menu?.querySelectorAll("a");
const isFocusable = document.querySelectorAll("a, button");
const isFocusableNotLinks = focusableNotLinks(isFocusable, links);
updateOnLoadAndResize(buttonParent, menu, links, isFocusableNotLinks);
window.addEventListener("resize", () =>
updateOnLoadAndResize(buttonParent, menu, links, isFocusableNotLinks),
);
button?.addEventListener("click", () => {
toggleMenu(menu, links, isFocusableNotLinks);
});
links?.forEach((link) =>
link.addEventListener("click", () =>
closeMenu(menu, links, isFocusableNotLinks),
),
);
document.addEventListener("click", (e) => {
const element = e.target as Element;
//@ts-ignore
if (!element.closest(menu.tagName) && e.target !== button) {
closeMenu(menu, links, isFocusableNotLinks);
}
});
document.addEventListener("keyup", (e) => {
if (e.key === "Escape") {
closeMenu(menu, links, isFocusableNotLinks);
}
});
}
export { startMenu };toggleMenu
type NonSpecificElement = Element | null;
type Links = NodeListOf<HTMLAnchorElement> | undefined;
type FocusableElements = NodeListOf<Element> | undefined;
type Tabindex = "0" | "-1";
const buttonParent = document.querySelector(".burger-button");
const button = buttonParent?.querySelector(".burger-button button");
function updateOnLoadAndResize(
buttonParent: NonSpecificElement,
menu: NonSpecificElement,
links: Links,
focusableElements: Element[],
) {
buttonParent && getComputedStyle(buttonParent).display === "none"
? setDesktopView(menu, links, focusableElements)
: setTabindex(links, "-1");
}
function setDesktopView(
menu: NonSpecificElement,
links: Links,
focusableElements: Element[],
) {
removeClassOpen(menu);
setTabindex(links, "0");
removeFocusTrap(focusableElements);
removeAriaExpanded();
}
function setTabindex(links: Links | Element[], number: Tabindex) {
links?.forEach((link) => link.setAttribute("tabindex", number));
}
function removeClassOpen(menu: NonSpecificElement) {
menu?.classList.remove("open");
}
function removeFocusTrap(focusableElements: Element[]) {
setTabindex(focusableElements, "0");
}
function toggleMenu(
menu: NonSpecificElement,
links: Links,
focusableElements: Element[],
) {
menu?.classList.toggle("open");
const tabindex = defineTabindex(menu);
setTabindex(links, tabindex);
toggleFocusTrap(focusableElements, tabindex);
updateAriaExpanded();
}
function defineTabindex(menu: NonSpecificElement) {
return menu?.classList.contains("open") ? "0" : "-1";
}
function toggleFocusTrap(focusableElements: Element[], number: Tabindex) {
const invertedNumber = number === "0" ? "-1" : "0";
setTabindex(focusableElements, invertedNumber);
}
function closeMenu(
menu: NonSpecificElement,
links: Links,
focusableElements: Element[],
) {
if (buttonParent && getComputedStyle(buttonParent).display !== "none") {
removeClassOpen(menu);
setTabindex(links, "-1");
removeFocusTrap(focusableElements);
removeAriaExpanded();
}
}
function updateAriaExpanded() {
if (button) {
const expandedAsBoolean = button.getAttribute("aria-expanded") === "true";
const newValue = !expandedAsBoolean;
button.setAttribute("aria-expanded", newValue.toString());
}
}
function removeAriaExpanded() {
if (button) {
button.setAttribute("aria-expanded", "false");
}
}
function focusableNotLinks(focusableElements: FocusableElements, links: Links) {
const elements = [];
if (focusableElements && links) {
for (let i = 0; i < focusableElements.length; i++) {
for (let j = 0; j < links.length; j++) {
if (focusableElements[i] === links[j]) {
break;
}
if (j === links.length - 1) {
elements.push(focusableElements[i]);
}
}
}
}
return elements;
}
export { toggleMenu, updateOnLoadAndResize, focusableNotLinks, closeMenu };And it got to this:
menuEngine
import { Menu } from "./menuObject.ts";
function startMenu() {
const buttonParent = document.querySelector(".burger-button");
const menu = document.querySelector(".navigation-items");
const links = menu?.querySelectorAll("a");
const menuObject = new Menu(buttonParent, menu, links);
menuObject.updateOnLoadAndResize();
window.addEventListener("resize", () => menuObject.updateOnLoadAndResize());
document.addEventListener("click", (e) => {
if (
//@ts-ignore
!e.target.closest(menuObject.menu.tagName) &&
e.target !== menuObject.buttonParent
) {
menuObject.closeMenu();
}
});
document.addEventListener("keyup", (e) => {
if (e.key === "Escape") {
menuObject.closeMenu();
}
});
}
export { startMenu };menuObject
import type { GenericElement, Links } from "../types/menuEngine";
class Menu {
buttonParent;
button;
menu;
links;
isFocusable = document.querySelectorAll("a, button");
focusableNotLinks;
isOpen = false;
isDesktop;
constructor(buttonParent: GenericElement, menu: GenericElement, links: Links) {
this.buttonParent = buttonParent;
this.button = buttonParent?.querySelector(".burger-button button");
this.menu = menu;
this.links = links;
this.focusableNotLinks = this.linksNotMenu();
this.isDesktop =
this.buttonParent && getComputedStyle(this.buttonParent).display === "none";
this.buttonParent?.addEventListener("click", () => this.toggleMenu());
this.links?.forEach((link) =>
link.addEventListener("click", () => this.closeMenu()),
);
}
toggleMenu() {
this.toggleIsOpen();
this.updateSettings();
}
closeMenu() {
if (!this.isDesktop) {
this.closeIsOpen();
this.updateSettings();
}
}
closeIsOpen() {
this.isOpen = false;
}
toggleIsOpen() {
this.isOpen = !this.isOpen;
}
updateSettings() {
this.updateMenuClass();
this.setTabindex();
this.updateAriaExpanded();
}
updateMenuClass() {
if (this.isDesktop) {
this.menu?.classList.remove("open");
} else if (!this.isDesktop && this.isOpen) {
this.menu?.classList.add("open");
} else if (!this.isDesktop && !this.isOpen) {
this.menu?.classList.remove("open");
}
}
setTabindex() {
if (this.isDesktop) {
this.links?.forEach((link) => link.setAttribute("tabindex", "0"));
this.focusableNotLinks.forEach((item) => item.setAttribute("tabindex", "0"));
} else if (!this.isDesktop && this.isOpen) {
this.links?.forEach((link) => link.setAttribute("tabindex", "0"));
this.focusableNotLinks.forEach((item) => item.setAttribute("tabindex", "-1"));
this.button.setAttribute("tabindex", "0");
} else if (!this.isDesktop && !this.isOpen) {
this.links?.forEach((link) => link.setAttribute("tabindex", "-1"));
this.focusableNotLinks.forEach((item) => item.setAttribute("tabindex", "0"));
}
}
updateAriaExpanded() {
this.button?.setAttribute("aria-expanded", this.isOpen.toString());
}
updateOnLoadAndResize() {
if (this.buttonParent) {
this.isDesktop = getComputedStyle(this.buttonParent).display === "none";
this.isDesktop ? this.setDesktopView() : this.setMobileView();
}
}
setDesktopView() {
this.closeIsOpen();
this.updateSettings();
}
setMobileView() {
this.setTabindex();
}
linksNotMenu() {
const elements = [];
if (this.isFocusable && this.links) {
for (let i = 0; i < this.isFocusable.length; i++) {
for (let j = 0; j < this.links.length; j++) {
if (this.isFocusable[i] === this.links[j]) {
break;
}
if (j === this.links.length - 1) {
elements.push(this.isFocusable[i]);
}
}
}
}
return elements;
}
}
export { Menu };- Client side scripts with view transitions
- SVG inside anchor tag
- Creating layout with vertical and horizontal line
- Making arrays into types
- Creating dynamic tags to support different heading levels in different pages. I created a function to make the process easier:
import type { HeadingLevel } from "../types/headingLevel.ts";
function generateChildHeading(parentHeading: HeadingLevel) {
const parentLevelInteger = parseInt(parentHeading.slice(-1));
//@ts-ignore
const childHeading: HeadingLevel = `h${parentLevelInteger + 1}`;
return childHeading;
}
export { generateChildHeading };- I created a function to make slugs in url more uniform:
const notAlphaNumeric = /[^A-Za-z0-9]/g;
function normalizeString(str: string) {
return str.replace(notAlphaNumeric, "").toLowerCase();
}
export { normalizeString };- Creating my own domain and redirecting to it;
-
Start readme
-
Create gitignore
-
Configure Prettier and ESLint
-
Create staging environment and server
-
Install Sass
-
Reset CSS
-
Set project's typography
- Choose and import font-family
- Set sizes, weights and line-heights
-
Set project's colors
-
Set project's layout structure
- Set horizontal margins
- Set vertical margins and paddings
-
Structure (spacing, sizes, colors), responsiveness
- Header
- HTML
- Mobile
- Desktop
- Footer
- HTML
- Mobile
- Desktop
- About me
- HTML
- Mobile
- Desktop
- Projects
- HTML
- Mobile
- Desktop
- Articles
- HTML
- Mobile
- Desktop
- Header
-
Animation
- Menu
- User interaction with buttons
-
Selection and scrollbar
-
Content schema
- Navigation with keyboard (tab, esc, enter)
- Zoom 200%
- Font-size 32px
- Screens from 300px to 2500px
- Element's overflow