Exiros
Things I learned working in a recycled website

Hey!
I'm Agus
- π I like most working in UI
- π©βπ I've been working here for a year and a half
- πΏ TS is my nemesis
- π I get reaaally nervous when I have to talk to a lot of people

Things we did different
(and I liked them π )
Hero Slider π
The before π
import AbstractTransitionBlock from 'app/component/block/AbstractTransitionBlock';
import LandingHeroTransitionController from './LandingHeroTransitionController';
import formatNumber from '../../../util/formatNumber';
export default class LandingHero extends AbstractTransitionBlock {
public static displayName: string = 'landing-hero';
public transitionController: LandingHeroTransitionController;
private readonly wrapper: HTMLDivElement;
private readonly list: HTMLUListElement;
private readonly listItems: Array<HTMLLIElement>;
private readonly images: Array<HTMLLIElement>;
private readonly prevButton: HTMLButtonElement;
private readonly nextButton: HTMLButtonElement;
private readonly currentCopy: HTMLParagraphElement;
private readonly progressLine: HTMLSpanElement;
private currentSlide: number = 0;
private swipeDirection: number = 0;
private posX: number = 0;
private posY: number = 0;
private isSwiping: boolean = false;
private windowWidth: number = window.innerWidth;
constructor(el: HTMLElement) {
super(el);
this.transitionController = new LandingHeroTransitionController(this);
this.wrapper = this.getElement('[data-hero-wrapper]');
this.list = this.getElement('[data-hero-content-list]');
this.listItems = this.getElements('[data-hero-content-item]');
this.images = this.getElements('[data-hero-image]');
this.prevButton = this.getElement('[data-navigation-prev-button]');
this.nextButton = this.getElement('[data-navigation-next-button]');
this.currentCopy = this.getElement('[data-navigation-current-item]');
this.progressLine = this.getElement('[data-navigation-progress-line]');
}
public adopted() {
if (this.listItems.length > 1) {
this.updateSlideNavigation();
} else {
this.listItems[0].classList.add('current');
this.images[0].classList.add('current');
}
this.setWrapperHeight();
this.addEventListeners();
}
private setWrapperHeight = () => {
const maxHeight = this.listItems.reduce(
(height, item) => Math.max(height, item.getBoundingClientRect().height),
0,
);
this.list.style.height = `${maxHeight}px`;
};
private handlePrevClick = () => {
this.currentSlide -= 1;
this.updateSlideNavigation();
this.currentCopy.innerText = formatNumber(`${this.currentSlide + 1}`, 2);
};
private handleNextClick = () => {
this.currentSlide += 1;
this.updateSlideNavigation();
this.currentCopy.innerText = formatNumber(`${this.currentSlide + 1}`, 2);
};
private updateSlideNavigation(): void {
this.prevButton.disabled = this.currentSlide === 0;
this.nextButton.disabled = this.currentSlide === this.listItems.length - 1;
const progressPercentage = -100 + ((this.currentSlide + 1) * 100) / this.listItems.length;
this.progressLine.style.transform = `translateX(${progressPercentage}%)`;
this.listItems.forEach((slide, index) => {
if (index === this.currentSlide) {
slide.classList.remove('next');
slide.classList.remove('prev');
slide.classList.add('current');
this.images[index].classList.remove('next');
this.images[index].classList.remove('prev');
this.images[index].classList.add('current');
} else if (index < this.currentSlide) {
slide.classList.remove('next');
slide.classList.remove('current');
slide.classList.add('prev');
this.images[index].classList.remove('next');
this.images[index].classList.remove('current');
this.images[index].classList.add('prev');
} else if (index > this.currentSlide) {
slide.classList.remove('current');
slide.classList.remove('prev');
slide.classList.add('next');
this.images[index].classList.remove('current');
this.images[index].classList.remove('prev');
this.images[index].classList.add('next');
}
});
this.handleResize();
}
private handleTouchStart = (event: TouchEvent): void => {
this.posX = event.touches[0].clientX;
this.posY = event.touches[0].clientY;
this.isSwiping = true;
};
private handleTouchMove = (event: TouchEvent): void => {
const { clientX, clientY } = event.touches[0];
const diffX = (100 * (this.posX - clientX)) / this.windowWidth;
const diffY = (100 * (this.posY - clientY)) / this.windowWidth;
/* The if condition will consider a "short" or "long" swipe and trigger
* either a 1 slide scroll, or an all slide scroll */
if ((Math.abs(diffX) > 20 && this.isSwiping) || Math.abs(diffX) > 35) {
switch (this.swipeDirection) {
case 0:
this.swipeDirection = Math.abs(diffX) > Math.abs(diffY) ? 1 : -1;
break;
case 1:
if (diffX < 0) {
this.posX = clientX;
if (this.currentSlide !== 0) {
this.handlePrevClick();
this.isSwiping = false;
}
} else {
event.preventDefault();
this.posX = clientX;
if (this.currentSlide < this.listItems.length - 1) {
this.handleNextClick();
this.isSwiping = false;
}
}
default:
break;
}
}
};
private handleTouchEnd = (event: TouchEvent): void => {
this.swipeDirection = 0;
};
private handleResize = (): void => {
if (this.windowWidth !== window.innerWidth) {
this.list.style.height = 'auto';
this.setWrapperHeight();
this.windowWidth = window.innerWidth;
}
};
private addEventListeners(): void {
window.addEventListener('resize', this.handleResize);
if (this.listItems.length > 1) {
this.wrapper.addEventListener('touchmove', this.handleTouchMove);
this.wrapper.addEventListener('touchstart', this.handleTouchStart);
this.wrapper.addEventListener('touchend', this.handleTouchEnd);
this.prevButton.addEventListener('click', this.handlePrevClick);
this.nextButton.addEventListener('click', this.handleNextClick);
}
}
private removeEventListeners(): void {
window.removeEventListener('resize', this.handleResize);
if (this.listItems.length > 1) {
this.wrapper.removeEventListener('touchmove', this.handleTouchMove);
this.wrapper.removeEventListener('touchstart', this.handleTouchStart);
this.wrapper.removeEventListener('touchend', this.handleTouchEnd);
this.prevButton.removeEventListener('click', this.handlePrevClick);
this.nextButton.removeEventListener('click', this.handleNextClick);
}
}
public dispose() {
this.removeEventListeners();
super.dispose();
}
}
Creating a slider from scratch is not always the best and most of the times it's like reinventing the wheel π‘
import AbstractBlock from 'app/component/block/AbstractBlock';
export default class C01SliderHero extends AbstractBlock {
public static displayName: string = 'c01-slider-hero';
constructor(el: any) {
super(el);
//@ts-ignore
const slider = tns({
container: '[data-tiny-slider]',
mode: 'gallery',
animateIn: 'transition-in',
animateOut: 'transition-out',
items: 1,
slideBy: 'page',
autoplay: true,
arrowKeys: true,
controlsPosition: 'bottom',
controlsContainer: '.controls-container',
navContainer: '.nav-container',
autoplayButtonOutput: false,
speed: 1000,
});
const buttons = this.getElements('[data-control]');
buttons.forEach(el => el.addEventListener('click', this.stopAnim));
}
stopAnim = () => {
this.element.classList.add('no-anim');
};
public dispose() {
super.dispose();
const buttons = this.getElements('[data-control]');
buttons.forEach(el => el.removeEventListener('click', this.stopAnim));
}
}
The after π
We use a really small library (tiny-slider) with lots of options for customization, and with less than 50 LoC and some css magic we made it work π§ββοΈ

Accordeon π₯
The before π
import AbstractBlock from '../AbstractBlock';
export default class C10Accordion extends AbstractBlock {
public static readonly displayName: string = 'c10-accordion';
private readonly accordionButtons: ReadonlyArray<HTMLDivElement> = this.getElements(
'[data-accordion-button]',
);
private readonly accordionPanels: ReadonlyArray<HTMLDivElement> = this.getElements(
'[data-accordion-panel]',
);
private readonly accordionTitles: ReadonlyArray<HTMLDivElement> = this.getElements(
'[data-accordion-title]',
);
private readonly accordionItems: ReadonlyArray<HTMLDivElement> = this.getElements(
'[data-accordion-content]',
);
private prevIndex: number;
private accordionButtonListeners: Array<EventListener> = [];
constructor(el: HTMLElement) {
super(el);
this.prevIndex = 0;
this.displayFirstItem();
this.addEventListeners();
}
private toggleDisplay = (index: number) => {
const clickedButton = this.accordionButtons[index];
const clickedPanel = this.accordionPanels[index];
clickedButton.classList.toggle('active');
clickedPanel.style.display === 'block'
? (clickedPanel.style.display = 'none')
: (clickedPanel.style.display = 'block');
};
private displayFirstItem = () => {
const firstItem = this.accordionItems[0];
firstItem.classList.toggle('active');
};
private toggleItem = (index: number) => {
const prevItem = this.accordionItems[this.prevIndex];
const clickedItem = this.accordionItems[index];
prevItem.classList.toggle('active');
clickedItem.classList.toggle('active');
this.prevIndex = index;
};
private addEventListeners(): void {
this.accordionButtons.forEach((element: HTMLElement, index: number) => {
this.addListener(element, 'click', () => this.toggleDisplay(index));
});
this.accordionTitles.forEach((element: HTMLElement, index: number) => {
this.addListener(element, 'click', () => this.toggleItem(index));
});
}
public dispose() {
super.dispose();
}
}
Accordeons were a lot of fun in the past, small, simple component to create with javascript β³
The after π€
<details>
<summary>Accordeon Title</summary>
Content inside the accordeon. Yes! It just works! and it supports <span>more html tags!</span>.
</details>
But did you know accordeons (as <details> and <summary> tags) are part of the HTML spec now! They don't even need JS (maybe a little of css though) π€―

Custom cursor πΉ

The before π
import AbstractComponent from '../../AbstractComponent';
import { TweenLite, Power3 } from 'gsap';
import lerp from '../../../util/lerp';
import bowser from 'bowser';
export default class M14CustomCursor extends AbstractComponent {
public static displayName: string = 'm14-custom-cursor';
private static readonly IS_ACTIVE: string = 'is-active';
private blocks: Array<HTMLElement> = [];
private isDesktop: boolean =
(bowser.getParser(window.navigator.userAgent) as any).parsedResult.platform.type === 'desktop';
private cursorSize: number = 0;
private scale: number = 1;
private isAnimating: boolean = false;
private mousePosition: { x: number; y: number; lastX: number; lastY: number } = {
x: 0,
y: 0,
lastX: 0,
lastY: 0,
};
constructor(el: HTMLElement) {
super(el);
}
public adopted() {
if (this.isDesktop) {
this.blocks = [].slice.call(document.querySelectorAll('[data-custom-cursor]'));
this.cursorSize = this.element.clientWidth;
this.updatePosition();
this.addEventListeners();
}
}
private handleMouseMove = (event: MouseEvent) => {
this.mousePosition.x = event.clientX - this.cursorSize * 0.5;
this.mousePosition.y = event.clientY + this.cursorSize * 0.25;
};
private handleMouseDown = () => {
TweenLite.to(this, 0.6, { scale: 0.85, ease: Power3.easeOut });
if (this.isAnimating) document.body.style.cursor = 'grabbing';
};
private handleMouseUp = () => {
TweenLite.to(this, 0.4, { scale: 1, ease: Power3.easeOut });
if (this.isAnimating) document.body.style.cursor = 'grab';
};
private updatePosition = () => {
requestAnimationFrame(this.updatePosition);
this.mousePosition.lastX = lerp(this.mousePosition.lastX, this.mousePosition.x, 0.1);
this.mousePosition.lastY = lerp(this.mousePosition.lastY, this.mousePosition.y, 0.1);
if (!this.isAnimating) return;
this.element.style.transform = `translate3d(${this.mousePosition.lastX}px, ${
this.mousePosition.lastY
}px, 0) scale(${this.scale})`;
};
private handleMouseEnter = () => {
this.isAnimating = true;
document.body.style.cursor = 'grab';
this.element.classList.add(M14CustomCursor.IS_ACTIVE);
};
private handleMouseLeave = () => {
this.isAnimating = false;
document.body.style.cursor = '';
this.element.classList.remove(M14CustomCursor.IS_ACTIVE);
};
private addEventListeners = () => {
this.blocks.forEach((element: HTMLElement) => {
element.addEventListener('mouseenter', this.handleMouseEnter);
element.addEventListener('mouseleave', this.handleMouseLeave);
});
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mousedown', this.handleMouseDown);
document.addEventListener('mouseup', this.handleMouseUp);
};
private removeEventListeners = () => {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mousedown', this.handleMouseDown);
document.removeEventListener('mouseup', this.handleMouseUp);
};
public dispose() {
this.removeEventListeners();
super.dispose();
}
}
This cursor is really nice, but in order to make some changes requested, it was going to take a lot of time (remember TS is my nemesis πΏ)
The after π
.slider {
cursor: url("../../../../static/drag.svg"), pointer;
}
CSS TO THE RESCUE! YES! π
And remember the slider behind the moving
cursor? π
import AbstractBlock from '../AbstractBlock';
import { scrollTransition, baseComponentTransition } from '../../../util/transitions';
export default class C09TimelineCarousel extends AbstractBlock {
public static readonly displayName: string = 'c09-timeline-carousel';
constructor(el: HTMLElement) {
super(el);
//@ts-ignore
const slider = tns({
container: '[data-tiny-slider]',
mouseDrag: true,
loop: false,
fixedWidth: 240,
controls: false,
nav: false,
gutter: 52,
responsive: {
1024: {
fixedWidth: 305,
gutter: 185,
},
},
});
const transitionItems = this.getElements('[data-transition-item]');
scrollTransition(el, baseComponentTransition(transitionItems));
}
public dispose() {
super.dispose();
}
}
We also used the previous library π
AND π IT π JUST π WORKS π (yes, media-queries and all)
Offset columns?

Who knows you papΓ‘?

@mixin offsetFullWidth($columnsWanted, $total: 12, $direction) {
$columnsTotal: $total;
$magicNumber: 20;
@if $columnsTotal == 12 {
$magicNumber: 13.333;
}
$gapSize: 55;
$containerPadding: 80;
$percentage: (($columnsWanted / $columnsTotal) * 100) * 1%;
$offset: ($gapSize / $columnsTotal) * ($columnsTotal - $columnsWanted) + px;
$finalOffset: $containerPadding + ($gapSize - ($columnsWanted * $magicNumber)) + px;
width: 100%;
#{$direction}: calc(#{$percentage} - #{$offset} + #{$finalOffset});
}
SASS Mixings to the rescue π¦ΈββοΈ
Instead of doing magic with javascript every time the user loads and resizes we figured out we could rely on just CSS (don't know why the 20 works btw π§ββοΈ )
Conclusion

- No, most of the work it's not already done.
- It's better to start over that trying to adapt the new design with lots of hacks.
- You don't necessary have to use the same old implementations.
Thank you!

Exiros -
By Gabriel Miranda
Exiros -
- 208