A small web app for watching movies and shows easily
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

206 lines
5.8 KiB

import { Icon, Icons } from "@/components/Icon";
import { Spinner } from "@/components/layout/Spinner";
import { ProgressRing } from "@/components/layout/ProgressRing";
import { createRef, useEffect, useRef } from "react";
interface PopoutListEntryBaseTypes {
active?: boolean;
children: React.ReactNode;
onClick?: () => void;
isOnDarkBackground?: boolean;
}
interface PopoutListEntryTypes extends PopoutListEntryBaseTypes {
percentageCompleted?: number;
loading?: boolean;
errored?: boolean;
}
interface PopoutListEntryRootTypes extends PopoutListEntryBaseTypes {
right?: React.ReactNode;
noChevron?: boolean;
}
interface PopoutListActionTypes extends PopoutListEntryBaseTypes {
icon?: Icons;
right?: React.ReactNode;
download?: string;
href?: string;
noChevron?: boolean;
}
interface ScrollToActiveProps {
children: React.ReactNode;
className?: string;
}
interface PopoutSectionProps {
children?: React.ReactNode;
className?: string;
}
export function ScrollToActive(props: ScrollToActiveProps) {
const ref = createRef<HTMLDivElement>();
const inited = useRef<boolean>(false);
// Scroll to "active" child on first load (AKA mount except React dumb)
useEffect(() => {
if (inited.current) return;
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
// Find nearest scroll container, or self
const wrapper: HTMLDivElement | null = el.classList.contains(
"overflow-y-auto"
)
? el
: el.closest(".overflow-y-auto");
const active: HTMLDivElement | null | undefined =
wrapper?.querySelector(".active");
if (wrapper && active) {
let activeYPositionCentered = 0;
const setActiveYPositionCentered = () => {
activeYPositionCentered =
active.getBoundingClientRect().top -
wrapper.getBoundingClientRect().top +
active.offsetHeight / 2;
};
setActiveYPositionCentered();
if (activeYPositionCentered >= wrapper.offsetHeight / 2) {
// Check if the active element is below the vertical center line, then scroll it into center
wrapper.scrollTo({
top: activeYPositionCentered - wrapper.offsetHeight / 2,
});
}
setActiveYPositionCentered();
if (activeYPositionCentered > wrapper.offsetHeight / 2) {
// If the element is over the vertical center line, scroll to the end
wrapper.scrollTo({
top: wrapper.scrollHeight,
});
}
}
inited.current = true;
}, [ref]);
return (
<div className={props.className} ref={ref}>
{props.children}
</div>
);
}
export function PopoutSection(props: PopoutSectionProps) {
return (
<ScrollToActive className={["p-5", props.className || ""].join(" ")}>
{props.children}
</ScrollToActive>
);
}
export function PopoutListEntryBase(props: PopoutListEntryRootTypes) {
const bg = props.isOnDarkBackground ? "bg-ash-200" : "bg-ash-400";
const hover = props.isOnDarkBackground
? "hover:bg-ash-200"
: "hover:bg-ash-400";
return (
<div
className={[
"group -mx-2 flex cursor-pointer items-center justify-between space-x-1 rounded p-2 font-semibold transition-[background-color,color] duration-150",
hover,
props.active
? `${bg} active text-white outline-denim-700`
: "text-denim-700 hover:text-white",
].join(" ")}
onClick={props.onClick}
>
{props.active && (
<div className="absolute left-0 h-8 w-0.5 bg-bink-500" />
)}
<span className="truncate">{props.children}</span>
<div className="relative min-h-[1rem] min-w-[1rem]">
{!props.noChevron && (
<Icon
className="absolute inset-0 translate-x-2 text-white opacity-0 transition-[opacity,transform] duration-100 group-hover:translate-x-0 group-hover:opacity-100"
icon={Icons.CHEVRON_RIGHT}
/>
)}
{props.right}
</div>
</div>
);
}
export function PopoutListEntry(props: PopoutListEntryTypes) {
return (
<PopoutListEntryBase
isOnDarkBackground={props.isOnDarkBackground}
active={props.active}
onClick={props.onClick}
noChevron={props.loading || props.errored}
right={
<>
{props.errored && (
<Icon
icon={Icons.WARNING}
className="absolute inset-0 text-rose-400"
/>
)}
{props.loading && !props.errored && (
<Spinner className="absolute inset-0 text-base [--color:#9C93B5]" />
)}
{props.percentageCompleted && !props.loading && !props.errored ? (
<ProgressRing
className="absolute inset-0 text-bink-600 opacity-100 transition-[opacity] group-hover:opacity-0"
backingRingClassname="stroke-ash-500"
percentage={
props.percentageCompleted > 90 ? 100 : props.percentageCompleted
}
/>
) : (
""
)}
</>
}
>
{props.children}
</PopoutListEntryBase>
);
}
export function PopoutListAction(props: PopoutListActionTypes) {
const entry = (
<PopoutListEntryBase
active={props.active}
isOnDarkBackground={props.isOnDarkBackground}
right={props.right}
onClick={props.href ? undefined : props.onClick}
noChevron={props.noChevron}
>
<div className="flex items-center space-x-3">
{props.icon ? <Icon className="text-xl" icon={props.icon} /> : null}
<div>{props.children}</div>
</div>
</PopoutListEntryBase>
);
return props.href ? (
<a
href={props.href ? props.href : undefined}
rel="noreferrer"
target="_blank"
download={props.download ? props.download : undefined}
onClick={props.onClick}
>
{entry}
</a>
) : (
entry
);
}