mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
Merge pull request #41 from binaryoverload/fix/dropdown
Add season dropdown
This commit is contained in:
commit
863a9823f1
8 changed files with 1843 additions and 1653 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -21,3 +21,5 @@ node_modules
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
package-lock.json
|
1
src/assets/down-arrow.svg
Normal file
1
src/assets/down-arrow.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
After Width: | Height: | Size: 261 B |
|
@ -2,6 +2,7 @@
|
||||||
background-color: var(--card);
|
background-color: var(--card);
|
||||||
padding: 3rem 4rem;
|
padding: 3rem 4rem;
|
||||||
margin: 0 3rem;
|
margin: 0 3rem;
|
||||||
|
margin-bottom: 6rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: height 500ms ease-in-out;
|
transition: height 500ms ease-in-out;
|
||||||
|
@ -13,7 +14,6 @@
|
||||||
|
|
||||||
.card-wrapper {
|
.card-wrapper {
|
||||||
transition: height 500ms ease-in-out;
|
transition: height 500ms ease-in-out;
|
||||||
overflow: hidden;
|
|
||||||
width: 45rem;
|
width: 45rem;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
@media screen and (max-width: 700px) {
|
@media screen and (max-width: 700px) {
|
||||||
.card {
|
.card {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: 6rem;
|
||||||
padding: 3rem 2rem;
|
padding: 3rem 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,9 @@ import React from 'react';
|
||||||
import { TypeSelector } from './TypeSelector';
|
import { TypeSelector } from './TypeSelector';
|
||||||
import { NumberSelector } from './NumberSelector';
|
import { NumberSelector } from './NumberSelector';
|
||||||
import { VideoProgressStore } from '../lib/storage/VideoProgress'
|
import { VideoProgressStore } from '../lib/storage/VideoProgress'
|
||||||
|
import { SelectBox } from '../components/SelectBox';
|
||||||
import './EpisodeSelector.css'
|
import './EpisodeSelector.css'
|
||||||
|
import { useWindowSize } from '../hooks/useWindowSize';
|
||||||
|
|
||||||
export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode, streamData }) {
|
export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode, streamData }) {
|
||||||
const choices = episodes ? episodes.map(v => {
|
const choices = episodes ? episodes.map(v => {
|
||||||
|
@ -27,9 +29,21 @@ export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode,
|
||||||
}
|
}
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
|
const windowSize = useWindowSize()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="episodeSelector">
|
<div className="episodeSelector">
|
||||||
<TypeSelector setType={setSelectedSeason} selected={selectedSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} /><br></br>
|
{
|
||||||
|
(seasons.length > 0 && (windowSize.width <= 768 || seasons.length > 4)) ?
|
||||||
|
(
|
||||||
|
<SelectBox setSelectedItem={(index) => setSelectedSeason(seasons[index])} selectedItem={seasons.findIndex(s => s === selectedSeason)} options={seasons.map(season => { return {id: season, name: `Season ${season}` }})}/>
|
||||||
|
)
|
||||||
|
:
|
||||||
|
(
|
||||||
|
<TypeSelector setType={setSelectedSeason} selected={selectedSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<br></br>
|
||||||
<NumberSelector setType={(e) => setEpisode({episode: e, season: selectedSeason})} choices={choices} selected={(selectedSeason.toString() === currentSeason) ? currentEpisode : null} />
|
<NumberSelector setType={(e) => setEpisode({episode: e, season: selectedSeason})} choices={choices} selected={(selectedSeason.toString() === currentSeason) ? currentEpisode : null} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
107
src/components/SelectBox.css
Normal file
107
src/components/SelectBox.css
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&display=swap');
|
||||||
|
|
||||||
|
/* select box styling */
|
||||||
|
.select-box {
|
||||||
|
display: flex;
|
||||||
|
width: 200px;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box > * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .options-container {
|
||||||
|
max-height: 0;
|
||||||
|
width: calc( 100% - 12px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: var(--choice);
|
||||||
|
order: 1;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .selected {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
position: relative;
|
||||||
|
width: 188px;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--choice);
|
||||||
|
color: white;
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .selected::after {
|
||||||
|
content: "";
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
background: url(../assets/down-arrow.svg);
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 50%;
|
||||||
|
transition: transform 150ms;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.select-box .option .item {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .options-container.active {
|
||||||
|
max-height: 240px;
|
||||||
|
opacity: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .options-container.active + .selected::after {
|
||||||
|
transform: translateY(-50%) rotateX(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .options-container::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
background: #0d141f;
|
||||||
|
background: #81878f;
|
||||||
|
background: #f1f2f3;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .options-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #525861;
|
||||||
|
background: #81878f;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
.select-box .option {
|
||||||
|
padding: 12px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .option,
|
||||||
|
.selected {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .options-container .option:hover {
|
||||||
|
background: var(--choice-hover);
|
||||||
|
}
|
||||||
|
.select-box .options-container .option:hover .item {
|
||||||
|
color: var(--choice-active-text, var(--text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box .option .radio {
|
||||||
|
display: none;
|
||||||
|
}
|
71
src/components/SelectBox.js
Normal file
71
src/components/SelectBox.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { useRef, useState, useEffect } from "react"
|
||||||
|
import "./SelectBox.css"
|
||||||
|
|
||||||
|
function Option({ option, onClick }) {
|
||||||
|
return (
|
||||||
|
<div className="option" onClick={onClick}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio"
|
||||||
|
id={option.id} />
|
||||||
|
<label htmlFor={option.id}>
|
||||||
|
<div className="item">{option.name}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectBox({ options, selectedItem, setSelectedItem }) {
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
throw new Error("Items must be an array!")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [active, setActive] = useState(false)
|
||||||
|
|
||||||
|
const containerRef = useRef();
|
||||||
|
|
||||||
|
const handleClick = e => {
|
||||||
|
if (containerRef.current.contains(e.target)) {
|
||||||
|
// inside click
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// outside click
|
||||||
|
closeDropdown()
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
setActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// add when mounted
|
||||||
|
document.addEventListener("mousedown", handleClick);
|
||||||
|
//document.addEventListener("scroll", closeDropdown);
|
||||||
|
// return function to be called when unmounted
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClick);
|
||||||
|
document.removeEventListener("scroll", closeDropdown)
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOptionClick = (e, option, i) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setSelectedItem(i)
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="select-box" ref={containerRef} onClick={() => setActive(a => !a)}>
|
||||||
|
<div className={"options-container" + (active ? " active" : "")}>
|
||||||
|
{options.map((opt, i) => (
|
||||||
|
<Option option={opt} key={i} onClick={(e) => onOptionClick(e, opt, i)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="selected">
|
||||||
|
{options ? (
|
||||||
|
<Option option={options[selectedItem]} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
28
src/hooks/useWindowSize.js
Normal file
28
src/hooks/useWindowSize.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// https://usehooks.com/useWindowSize/
|
||||||
|
export function useWindowSize() {
|
||||||
|
// Initialize state with undefined width/height so server and client renders match
|
||||||
|
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
||||||
|
const [windowSize, setWindowSize] = useState({
|
||||||
|
width: undefined,
|
||||||
|
height: undefined,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
// Handler to call on window resize
|
||||||
|
function handleResize() {
|
||||||
|
// Set window width/height to state
|
||||||
|
setWindowSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Add event listener
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
// Call handler right away so state gets updated with initial window size
|
||||||
|
handleResize();
|
||||||
|
// Remove event listener on cleanup
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, []); // Empty array ensures that effect is only run on mount
|
||||||
|
return windowSize;
|
||||||
|
}
|
Loading…
Reference in a new issue