Skip to content

Commit

Permalink
refactor(EpisodePicker): improve styles and typings
Browse files Browse the repository at this point in the history
  • Loading branch information
Botsy committed Feb 11, 2025
1 parent 6ca94a2 commit 3f60df9
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 104 deletions.
90 changes: 60 additions & 30 deletions src/components/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (C) 2017-2025 Smart code 203358507

import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react';
import React, { ChangeEvent, forwardRef, useCallback, useEffect, useState } from 'react';
import { type KeyboardEvent, type InputHTMLAttributes } from 'react';
import classnames from 'classnames';
import styles from './styles.less';
Expand All @@ -18,10 +18,11 @@ type Props = InputHTMLAttributes<HTMLInputElement> & {
max?: number;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
onSubmit?: (event: KeyboardEvent<HTMLInputElement>) => void;
onUpdate?: (value: number) => void;
};

const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...props }, ref) => {
const [value, setValue] = useState(defaultValue || 0);
const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, showButtons, onUpdate, ...props }, ref) => {
const [value, setValue] = useState<number>(defaultValue || 0);
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
props.onKeyDown && props.onKeyDown(event);

Expand All @@ -32,33 +33,63 @@ const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...prop

const handleIncrease = () => {
const { max } = props;
if (typeof max !== 'undefined') {
return setValue((prevVal) =>
prevVal + 1 > max ? max : prevVal + 1
);
if (max !== undefined) {
return setValue((prevVal) => {
const value = prevVal || 0;
return value + 1 > max ? max : value + 1;
});
}
setValue((prevVal) => prevVal + 1);
setValue((prevVal) => {
const value = prevVal || 0;
return value + 1;
});
};

const handleDecrease = () => {
const { min } = props;
if (typeof min !== 'undefined') {
return setValue((prevVal) =>
prevVal - 1 < min ? min : prevVal - 1
);
if (min !== undefined) {
return setValue((prevVal) => {
const value = prevVal || 0;
return value - 1 < min ? min : value - 1;
});
}
setValue((prevVal) => prevVal - 1);
setValue((prevVal) => {
const value = prevVal || 0;
return value - 1;
});
};

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const min = props.min || 0;
let newValue = event.target.valueAsNumber;
if (newValue && newValue < min) {
newValue = min;
}
if (props.max !== undefined && newValue && newValue > props.max) {
newValue = props.max;
}
setValue(newValue);
};

useEffect(() => {
if (typeof onUpdate === 'function') {
onUpdate(value);
}
}, [value]);

return (
<div className={classnames(props.containerClassName, styles['number-input'])}>
{props.showButtons ? <Button
className={styles['btn']}
onClick={handleDecrease}
disabled={props.disabled || (props.min !== undefined ? value <= props.min : false)}>
<Icon className={styles['icon']} name={'remove'} />
</Button> : null}
<div className={classnames(styles['number-display'], props.showButtons ? styles['with-btns'] : '')}>
{
showButtons ?
<Button
className={styles['button']}
onClick={handleDecrease}
disabled={props.disabled || (props.min !== undefined ? value <= props.min : false)}>
<Icon className={styles['icon']} name={'remove'} />
</Button>
: null
}
<div className={classnames(styles['number-display'], showButtons ? styles['buttons-container'] : '')}>
{props.label && <div className={styles['label']}>{props.label}</div>}
<input
ref={ref}
Expand All @@ -67,19 +98,18 @@ const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...prop
value={value}
{...props}
className={classnames(props.className, styles['value'], { 'disabled': props.disabled })}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const newValue = parseInt(event.target.value);
if (props.min !== undefined && newValue < props.min) return props.min;
if (props.max !== undefined && newValue > props.max) return props.max;
setValue(newValue);
}}
onChange={handleChange}
onKeyDown={onKeyDown}
/>
</div>
{props.showButtons ? <Button
className={styles['btn']} onClick={handleIncrease} disabled={props.disabled || (props.max !== undefined ? value >= props.max : false)}>
<Icon className={styles['icon']} name={'add'} />
</Button> : null}
{
showButtons ?
<Button
className={styles['button']} onClick={handleIncrease} disabled={props.disabled || (props.max !== undefined ? value >= props.max : false)}>
<Icon className={styles['icon']} name={'add'} />
</Button>
: null
}
</div>
);
});
Expand Down
112 changes: 51 additions & 61 deletions src/components/NumberInput/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,63 @@
.number-input {
user-select: text;
display: flex;
max-width: 12rem;
max-width: 14rem;
height: 3.5rem;
margin-bottom: 1rem;
color: var(--primary-foreground-color);
background: var(--overlay-color);
border-radius: 100rem;
border-radius: 3.5rem;

.btn {
width: 2.875rem;
height: 2.875rem;
background: var(--overlay-color);
border: none;
border-radius: 50%;
color: var(--primary-foreground-color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.3s;
z-index: 1;

svg {
width: 1.625rem;
}
}

.number-display {
height: 2.875rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 1rem;

&::-moz-focus-inner {
.button {
flex: none;
width: 3.5rem;
height: 3.5rem;
padding: 1rem;
background: var(--overlay-color);
border: none;
}
border-radius: 100%;
cursor: pointer;
z-index: 1;

&.with-btns {
padding: 0 1.9375rem;
margin-left: -1.4375rem;
margin-right: -1.4375rem;
max-width: 9.125rem;
.icon {
width: 100%;
height: 100%;
}
}
}

/* Label */
.number-display .label {
font-size: 0.625rem;
font-weight: 400;
opacity: 0.7;
}

/* Value */
.number-display .value {
font-size: 1.3125rem;
display: flex;
width: 100%;
color: var(--primary-foreground-color);
text-align: center;

&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: textfield;
margin: 0;
.number-display {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 1rem;

&::-moz-focus-inner {
border: none;
}

.label {
font-size: 0.8rem;
font-weight: 400;
opacity: 0.7;
}

.value {
font-size: 1.2rem;
display: flex;
justify-content: center;
width: 100%;
color: var(--primary-foreground-color);
text-align: center;
appearance: none;

&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
flex: 0 1 auto;
font-size: 1rem;
font-weight: 700;
max-height: 3.6em;
max-height: 3.5rem;
text-align: center;
color: var(--primary-foreground-color);
margin-bottom: 0;
Expand Down
19 changes: 7 additions & 12 deletions src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (C) 2017-2025 Smart code 203358507

import React, { useRef } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button, NumberInput } from 'stremio/components';
import styles from './EpisodePicker.less';
Expand All @@ -16,33 +16,28 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => {
const videoId = decodeURIComponent(splitPath[splitPath.length - 1]);
const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : [];
const [season, setSeason] = React.useState(() => {
const initialSeason = isNaN(parseInt(pathSeason)) ? 1 : parseInt(pathSeason);
const initialSeason = isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason);
return initialSeason;
});
const [episode, setEpisode] = React.useState(() => {
const initialEpisode = isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode);
return initialEpisode;
});
const seasonRef = useRef<HTMLInputElement>(null);
const episodeRef = useRef<HTMLInputElement>(null);
const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 1);

const handleSeasonChange = (value?: number) => setSeason(value !== undefined ? value : 1);

const handleEpisodeChange = (value?: number) => setEpisode(value !== undefined ? value : 1);
const handleEpisodeChange = (value: number) => setEpisode(!isNaN(value) ? value : 1);

const handleSubmit = React.useCallback(() => {
const season = parseInt(seasonRef.current?.value || '1');
const episode = parseInt(episodeRef.current?.value || '1');
if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) {
onSubmit(season, episode);
}
}, [onSubmit, seasonRef, episodeRef]);
}, [onSubmit, season, episode]);

const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]);

return <div className={className}>
<NumberInput ref={seasonRef} min={0} label={t('SEASON')} defaultValue={season} onUpdate={handleSeasonChange} showButtons />
<NumberInput ref={episodeRef} min={1} label={t('EPISODE')} defaultValue={episode} onUpdate={handleEpisodeChange} showButtons />
<NumberInput min={0} label={t('SEASON')} placeholder={t('SPECIAL')} defaultValue={season} onUpdate={handleSeasonChange} showButtons />
<NumberInput min={1} label={t('EPISODE')} defaultValue={episode} onUpdate={handleEpisodeChange} showButtons />
<Button className={styles['button-container']} onClick={handleSubmit} disabled={disabled}>
<div className={styles['label']}>{t('SIDEBAR_SHOW_STREAMS')}</div>
</Button>
Expand Down

0 comments on commit 3f60df9

Please sign in to comment.