Skip to content

Commit

Permalink
feat(home): add home screen and slider
Browse files Browse the repository at this point in the history
  • Loading branch information
royschut committed May 4, 2021
1 parent ab15803 commit 17f1f00
Show file tree
Hide file tree
Showing 15 changed files with 471 additions and 94 deletions.
19 changes: 10 additions & 9 deletions .commitlintrc.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules : {
'scope-enum': [
2, 'always', [
'project',
],
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': [
2, 'always', [
'project',
'home',
'playlist'
],
},
};
],
},
};
12 changes: 3 additions & 9 deletions public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,13 @@
"content": [
{
"playlistId": "WXu7kuaW",
"featured": true,
"enableText": false
"featured": true
},
{
"playlistId": "lrYLc95e",
"aspectratio": "23",
"cols": {"xs": 2, "sm": 3, "md": 4, "lg": 5, "xl": 6}
"playlistId": "lrYLc95e"
},
{
"playlistId": "Q352cyuc",
"type": "grid",
"enableSeeAll": true,
"rows": 1
"playlistId": "Q352cyuc"
},
{
"playlistId": "oR7ahO0J"
Expand Down
2 changes: 2 additions & 0 deletions src/components/Shelf/Shelf.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@use '../../styles/variables';
@use '../../styles/theme';
11 changes: 11 additions & 0 deletions src/components/Shelf/Shelf.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import Shelf from './Shelf';

describe('FeaturedShelf Component tests', () => {
test.skip('dummy test', () => {
render(<Shelf></Shelf>);
expect(screen.getByText('hello world')).toBeInTheDocument();
});
});
101 changes: 101 additions & 0 deletions src/components/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useContext } from 'react';
import type { Config } from 'types/Config';

import { ConfigContext } from '../../providers/configProvider';
import TileDock from '../TileDock/TileDock';

import styles from './Shelf.module.scss';

export type Image = {
src: string;
type: string;
width: number;
};

export type ShelfProps = {
title: string;
playlist: string[];
featured: boolean;
};

export type Source = {
file: string;
type: string;
};

export type Track = {
file: string;
kind: string;
label: string;
};

export type Item = {
description: string;
duration: number;
feedid: string;
image: string;
images: Image[];
junction_id: string;
link: string;
mediaid: string;
pubdate: number;
sources: Source[];
tags: string;
title: string;
tracks: Track[];
variations: Record<string, unknown>;
};

const Shelf: React.FC<ShelfProps> = ({
title,
playlist,
featured,
}: ShelfProps) => {
const config: Config = useContext(ConfigContext);

return (
<div className={styles['Shelf']}>
<p>
Playlist {title} {featured}
</p>
<TileDock
items={playlist}
tilesToShow={6}
tileHeight={300}
cycleMode={'endless'}
transitionTime="0.3s"
spacing={3}
renderLeftControl={(handleClick) => (
<button onClick={handleClick}>Left</button>
)}
renderRightControl={(handleClick) => (
<button onClick={handleClick}>Right</button>
)}
renderTile={(item: unknown) => {
return (
<div
style={{
background: 'white',
width: '100%',
height: '100%',
overflow: 'hidden',
}}
>
<div
style={{
width: '100%',
height: '100%',
background: `url('${
(item as Item).images[0]?.src
}') center / cover no-repeat`,
}}
>
</div>
</div>
)}}
/>
</div>
);
};

export default Shelf;
35 changes: 35 additions & 0 deletions src/components/TileDock/TileDock.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.tileDock {
overflow: hidden;
position: relative;
}
.tileDock ul {
display: block;
white-space: nowrap;
margin: 0;
padding: 0;
}
.tileDock li {
display: inline-block;
list-style-type: none;
white-space: normal;
}
.tileDock .offsetTile {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.tileDock .leftControl {
left: 0px;
position: absolute;
top: 50%;
transform: translateY(-100%);
z-index: 1;
}
.tileDock .rightControl {
position: absolute;
right: 0px;
top: 50%;
transform: translateY(-100%);
}
196 changes: 196 additions & 0 deletions src/components/TileDock/TileDock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React, { useLayoutEffect, useRef, useState } from 'react';
import './TileDock.css';

export type CycleMode = 'stop' | 'restart' | 'endless';
type Direction = 'left' | 'right';
type Position = { x: number; y: number; };

export type TileDockProps = {
items: unknown[];
cycleMode?: CycleMode;
tilesToShow?: number;
spacing?: number;
tileHeight?: number;
minimalTouchMovement?: number;
showControls?: boolean;
animated?: boolean;
transitionTime?: string;
renderTile: (item: unknown, index: number) => JSX.Element;
renderLeftControl?: (handleClick: () => void) => JSX.Element;
renderRightControl?: (handleClick: () => void) => JSX.Element;
};

const TileDock = ({
items,
tilesToShow = 6,
cycleMode = 'endless',
spacing = 12,
tileHeight = 300,
minimalTouchMovement = 30,
showControls = true,
animated = !window.matchMedia('(prefers-reduced-motion)').matches,
transitionTime = '0.6s',
renderTile,
renderLeftControl,
renderRightControl,
}: TileDockProps) => {
const [index, setIndex] = useState<number>(0);
const [slideToIndex, setSlideToIndex] = useState<number>(0);
const [transform, setTransform] = useState<number>(-100);
const [doAnimationReset, setDoAnimationReset] = useState<boolean>(false);
const [touchPosition, setTouchPosition] = useState<Position>({ x: 0, y: 0 } as Position);
const frameRef = useRef<HTMLUListElement>() as React.MutableRefObject<HTMLUListElement>;
const tilesToShowRounded: number = Math.floor(tilesToShow);
const offset: number = Math.round((tilesToShow - tilesToShowRounded) * 10) / 10;
const offsetCompensation: number = offset ? 1 : 0;
const tileWidth: number = 100 / (tilesToShowRounded + offset * 2);
const isMultiPage: boolean = items.length > tilesToShowRounded;
const transformWithOffset: number = isMultiPage ? 100 - tileWidth * (tilesToShowRounded + offsetCompensation - offset) + transform : 0;

const sliceItems = (items: unknown[]): unknown[] => {
const sliceFrom: number = index;
const sliceTo: number = index + tilesToShowRounded * 3 + offsetCompensation * 2;
const cycleModeEndlessCompensation: number = cycleMode === 'endless' ? tilesToShowRounded : 0;
const listStartClone: unknown[] = items.slice(0, tilesToShowRounded + cycleModeEndlessCompensation + offsetCompensation,);
const listEndClone: unknown[] = items.slice(0 - (tilesToShowRounded + offsetCompensation));
const itemsWithClones: unknown[] = [...listEndClone, ...items, ...listStartClone];
const itemsSlice: unknown[] = itemsWithClones.slice(sliceFrom, sliceTo);

return itemsSlice;
};

const tileList: unknown[] = isMultiPage ? sliceItems(items) : items;
const isAnimating: boolean = index !== slideToIndex;
const transitionBasis: string = `transform ${animated ? transitionTime : '0s'} ease`;

const needControls: boolean = showControls && isMultiPage;
const showLeftControl: boolean = needControls && !(cycleMode === 'stop' && index === 0);
const showRightControl: boolean = needControls && !(cycleMode === 'stop' && index === items.length - tilesToShowRounded);

const slide = (direction: Direction) : void => {
const directionFactor = (direction === 'right')? 1 : -1;
let nextIndex: number = index + (tilesToShowRounded * directionFactor);

if(nextIndex < 0){
if (cycleMode === 'stop') nextIndex = 0;
if (cycleMode === 'restart') nextIndex = index === 0 ? 0 - tilesToShowRounded : 0;
}
if (nextIndex > items.length - tilesToShowRounded) {
if(cycleMode === 'stop') nextIndex = items.length - tilesToShowRounded;
if(cycleMode === 'restart') nextIndex = index === items.length - tilesToShowRounded ? items.length : items.length - tilesToShowRounded;
}

const steps: number = Math.abs(index - nextIndex);
const movement: number = steps * tileWidth * (0 - directionFactor);

setSlideToIndex(nextIndex);
setTransform(-100 + movement);
if (!animated) setDoAnimationReset(true);
};

const handleTouchStart = (event: React.TouchEvent): void => setTouchPosition({ x: event.touches[0].clientX, y: event.touches[0].clientY });
const handleTouchEnd = (event: React.TouchEvent): void => {
const newPosition = { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY };
const movementX: number = Math.abs(newPosition.x - touchPosition.x);
const movementY: number = Math.abs(newPosition.y - touchPosition.y);
const direction: Direction = (newPosition.x < touchPosition.x) ? 'right' : 'left';

if (movementX > minimalTouchMovement && movementX > movementY) {
slide(direction);
}
};

useLayoutEffect(() => {
const resetAnimation = (): void => {
let resetIndex: number = slideToIndex;

resetIndex = resetIndex >= items.length ? slideToIndex - items.length : resetIndex;
resetIndex = resetIndex < 0 ? items.length + slideToIndex : resetIndex;

setIndex(resetIndex);
if (frameRef.current) frameRef.current.style.transition = 'none';
setTransform(-100);
setTimeout(() => {
if (frameRef.current) frameRef.current.style.transition = transitionBasis;
}, 0);
setDoAnimationReset(false);
};

if (doAnimationReset) resetAnimation();
}, [
doAnimationReset,
index,
items.length,
slideToIndex,
tileWidth,
tilesToShowRounded,
transitionBasis,
]);

const renderGradientEdge = () : string => {
const firstPercentage = cycleMode === 'stop' && index === 0 ? offset * tileWidth : 0;
const secondPercentage = tileWidth * offset;
const thirdPercentage = 100 - tileWidth * offset;

return `linear-gradient(90deg, rgba(255,255,255,1) ${firstPercentage}%, rgba(255,255,255,0) ${secondPercentage}%, rgba(255,255,255,0) ${thirdPercentage}%, rgba(255,255,255,1) 100%)`;
};
const ulStyle = {
transform: `translate3d(${transformWithOffset}%, 0, 0)`,
// Todo: set capital W before creating package
webkitTransform: `translate3d(${transformWithOffset}%, 0, 0)`,
transition: transitionBasis,
marginLeft: -spacing / 2,
marginRight: -spacing / 2,
};

return (
<div className="tileDock" style={{ height: tileHeight }}>
{showLeftControl && !!renderLeftControl && (
<div className="leftControl">
{renderLeftControl(() => slide('left'))}
</div>
)}
<ul
ref={frameRef}
style={ulStyle}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onTransitionEnd={(): void => setDoAnimationReset(true)}
>
{tileList.map((item:any, listIndex) => {
const isVisible =
isAnimating ||
!isMultiPage ||
(listIndex > tilesToShowRounded - offsetCompensation - 1 &&
listIndex < tilesToShowRounded * 2 + offsetCompensation + offsetCompensation);

return (
<li
key={`visibleTile${listIndex}`}
style={{
width: `${tileWidth}%`,
height: tileHeight,
visibility: isVisible ? 'visible' : 'hidden',
paddingLeft: spacing / 2,
paddingRight: spacing / 2,
boxSizing: 'border-box',
}}
>
{renderTile(item, listIndex)}
</li>
);
})}
</ul>
{offsetCompensation > 0 && isMultiPage && (
<div className="offsetTile" style={{ background: renderGradientEdge() }} />
)}
{showRightControl && !!renderRightControl && (
<div className="rightControl">
{renderRightControl(() => slide('right'))}
</div>
)}
</div>
);
};

export default TileDock;
Loading

0 comments on commit 17f1f00

Please sign in to comment.