-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(home): add home screen and slider
- Loading branch information
Showing
15 changed files
with
471 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
], | ||
}, | ||
}; | ||
], | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@use '../../styles/variables'; | ||
@use '../../styles/theme'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.