1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-29 16:07:40 +01:00

media grids

This commit is contained in:
Jelle van Snik 2023-01-07 23:44:46 +01:00
parent 42402eb5c7
commit e7981539e6
16 changed files with 136 additions and 112 deletions

View file

@ -42,6 +42,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/line-clamp": "^0.4.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/node": "^17.0.15", "@types/node": "^17.0.15",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",

View file

@ -37,7 +37,7 @@ export function SearchBarInput(props: SearchBarProps) {
} }
return ( return (
<div className="relative flex flex-col rounded-[28px] bg-denim-300 transition-colors focus-within:bg-denim-400 hover:bg-denim-400 sm:flex-row sm:items-center"> <div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center">
<div className="pointer-events-none absolute left-5 top-0 bottom-0 flex max-h-14 items-center"> <div className="pointer-events-none absolute left-5 top-0 bottom-0 flex max-h-14 items-center">
<Icon icon={Icons.SEARCH} /> <Icon icon={Icons.SEARCH} />
</div> </div>

View file

@ -6,11 +6,7 @@ import React, {
} from "react"; } from "react";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { import { BackdropContainer, useBackdrop } from "@/components/layout/Backdrop";
Backdrop,
BackdropContainer,
useBackdrop,
} from "@/components/layout/Backdrop";
import { ButtonControlProps, ButtonControl } from "./ButtonControl"; import { ButtonControlProps, ButtonControl } from "./ButtonControl";
export interface OptionItem { export interface OptionItem {

View file

@ -12,9 +12,9 @@ export function IconPatch(props: IconPatchProps) {
return ( return (
<div className={props.className || undefined} onClick={props.onClick}> <div className={props.className || undefined} onClick={props.onClick}>
<div <div
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent bg-denim-300 transition-[color,transform,border-color] duration-75 ${ className={`flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent bg-denim-500 transition-[color,transform,border-color] duration-75 ${
props.clickable props.clickable
? "cursor-pointer hover:scale-110 hover:bg-denim-400 hover:text-white active:scale-125" ? "cursor-pointer hover:scale-110 hover:bg-denim-600 hover:text-white active:scale-125"
: "" : ""
} ${props.active ? "border-bink-600 bg-bink-100 text-bink-600" : ""}`} } ${props.active ? "border-bink-600 bg-bink-100 text-bink-600" : ""}`}
> >

View file

@ -40,7 +40,7 @@ export function useBackdrop(): [
return [setBackdrop, backdropProps, highlightedProps]; return [setBackdrop, backdropProps, highlightedProps];
} }
export function Backdrop(props: BackdropProps) { function Backdrop(props: BackdropProps) {
const clickEvent = props.onClick || (() => {}); const clickEvent = props.onClick || (() => {});
const animationEvent = props.onBackdropHide || (() => {}); const animationEvent = props.onBackdropHide || (() => {});
const [isVisible, setVisible, fadeProps] = useFade(); const [isVisible, setVisible, fadeProps] = useFade();
@ -59,7 +59,7 @@ export function Backdrop(props: BackdropProps) {
return ( return (
<div <div
className={`fixed left-0 right-0 top-0 h-screen w-screen bg-black bg-opacity-50 opacity-100 transition-opacity ${ className={`pointer-events-auto fixed left-0 right-0 top-0 h-screen w-screen bg-black bg-opacity-50 opacity-100 transition-opacity ${
!isVisible ? "opacity-0" : "" !isVisible ? "opacity-0" : ""
}`} }`}
{...fadeProps} {...fadeProps}
@ -99,9 +99,9 @@ export function BackdropContainer(
return ( return (
<div ref={root}> <div ref={root}>
{createPortal( {createPortal(
<div className="absolute top-0 left-0 z-[999]"> <div className="pointer-events-none fixed top-0 left-0 z-[999]">
<Backdrop active={props.active} {...props} /> <Backdrop active={props.active} {...props} />
<div ref={copy} className="absolute"> <div ref={copy} className="pointer-events-auto absolute">
{props.children} {props.children}
</div> </div>
</div>, </div>,

View file

@ -6,13 +6,14 @@ export function BrandPill(props: { clickable?: boolean }) {
return ( return (
<div <div
className={`flex items-center space-x-2 rounded-full bg-bink-100 bg-opacity-50 px-4 py-2 text-bink-600 ${props.clickable className={`flex items-center space-x-2 rounded-full bg-bink-300 bg-opacity-50 px-4 py-2 text-bink-600 ${
? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95" props.clickable
? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-400 hover:text-bink-700 active:scale-95"
: "" : ""
}`} }`}
> >
<Icon className="text-xl" icon={Icons.MOVIE_WEB} /> <Icon className="text-xl" icon={Icons.MOVIE_WEB} />
<span className="font-semibold text-white">{t('global.name')}</span> <span className="font-semibold text-white">{t("global.name")}</span>
</div> </div>
); );
} }

View file

@ -0,0 +1,18 @@
import { ReactNode } from "react";
interface WideContainerProps {
classNames?: string;
children?: ReactNode;
}
export function WideContainer(props: WideContainerProps) {
return (
<div
className={`mx-auto w-[700px] max-w-full px-8 sm:px-4 ${
props.classNames || ""
}`}
>
{props.children}
</div>
);
}

View file

@ -5,23 +5,20 @@ import {
MWMediaMeta, MWMediaMeta,
MWMediaType, MWMediaType,
} from "@/providers"; } from "@/providers";
import { Icon, Icons } from "@/components/Icon";
import { serializePortableMedia } from "@/hooks/usePortableMedia"; import { serializePortableMedia } from "@/hooks/usePortableMedia";
import { DotList } from "@/components/text/DotList"; import { DotList } from "@/components/text/DotList";
export interface MediaCardProps { export interface MediaCardProps {
media: MWMediaMeta; media: MWMediaMeta;
// eslint-disable-next-line react/no-unused-prop-types
watchedPercentage: number; watchedPercentage: number;
linkable?: boolean; linkable?: boolean;
series?: boolean; series?: boolean;
} }
function MediaCardContent({ // TODO add progress back
media,
linkable, function MediaCardContent({ media, series, linkable }: MediaCardProps) {
watchedPercentage,
series,
}: MediaCardProps) {
const provider = getProviderFromId(media.providerId); const provider = getProviderFromId(media.providerId);
if (!provider) { if (!provider) {
@ -29,52 +26,31 @@ function MediaCardContent({
} }
return ( return (
<article <div
className={`group relative mb-4 flex overflow-hidden rounded bg-denim-300 py-4 px-5 ${ className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
linkable ? "hover:bg-denim-400" : "" linkable ? "hover:bg-opacity-100" : ""
}`} }`}
> >
{/* progress background */} <article
{watchedPercentage > 0 ? ( className={`relative mb-2 p-3 transition-transform duration-100 ${
<div className="absolute top-0 left-0 right-0 bottom-0"> linkable ? "group-hover:scale-95" : ""
<div }`}
className="relative h-full bg-bink-300 bg-opacity-30" >
style={{ <div className="mb-4 aspect-[2/3] w-full rounded-xl bg-denim-500" />
width: `${watchedPercentage}%`, <h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
}} <span>{media.title}</span>
> {series && media.seasonId && media.episodeId ? (
<div className="absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l from-bink-400 to-transparent opacity-40" /> <span className="ml-2 text-xs text-denim-700">
</div> S{media.seasonId} E{media.episodeId}
</div> </span>
) : null} ) : null}
</h1>
<div className="relative flex flex-1"> <DotList
{/* card content */} className="text-xs"
<div className="flex-1"> content={[provider.displayName, media.mediaType, media.year]}
<h1 className="mb-1 font-bold text-white"> />
{media.title} </article>
{series && media.seasonId && media.episodeId ? ( </div>
<span className="ml-2 text-xs text-denim-700">
S{media.seasonId} E{media.episodeId}
</span>
) : null}
</h1>
<DotList
className="text-xs"
content={[provider.displayName, media.mediaType, media.year]}
/>
</div>
{/* hoverable chevron */}
<div
className={`flex translate-x-3 items-center justify-end text-xl text-white opacity-0 transition-[opacity,transform] ${
linkable ? "group-hover:translate-x-0 group-hover:opacity-100" : ""
}`}
>
<Icon icon={Icons.CHEVRON_RIGHT} />
</div>
</div>
</article>
); );
} }

View file

@ -0,0 +1,11 @@
interface MediaGridProps {
children?: React.ReactNode;
}
export function MediaGrid(props: MediaGridProps) {
return (
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3">
{props.children}
</div>
);
}

View file

@ -1,10 +1,15 @@
export interface TitleProps { export interface TitleProps {
children?: React.ReactNode; children?: React.ReactNode;
className?: string;
} }
export function Title(props: TitleProps) { export function Title(props: TitleProps) {
return ( return (
<h1 className="mx-auto max-w-xs text-2xl font-bold text-white sm:text-3xl md:text-4xl"> <h1
className={`text-2xl font-bold text-white sm:text-3xl md:text-4xl ${
props.className ?? ""
}`}
>
{props.children} {props.children}
</h1> </h1>
); );

View file

@ -1,5 +1,6 @@
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading"; import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { import {
getIfBookmarkedFromPortable, getIfBookmarkedFromPortable,
@ -20,9 +21,14 @@ function Bookmarks() {
title={t("search.bookmarks") || "Bookmarks"} title={t("search.bookmarks") || "Bookmarks"}
icon={Icons.BOOKMARK} icon={Icons.BOOKMARK}
> >
{bookmarks.map((v) => ( <MediaGrid>
<WatchedMediaCard key={[v.mediaId, v.providerId].join("|")} media={v} /> {bookmarks.map((v) => (
))} <WatchedMediaCard
key={[v.mediaId, v.providerId].join("|")}
media={v}
/>
))}
</MediaGrid>
</SectionHeading> </SectionHeading>
); );
} }
@ -44,13 +50,15 @@ function Watched() {
title={t("search.continueWatching") || "Continue Watching"} title={t("search.continueWatching") || "Continue Watching"}
icon={Icons.CLOCK} icon={Icons.CLOCK}
> >
{watchedItems.map((v) => ( <MediaGrid>
<WatchedMediaCard {watchedItems.map((v) => (
key={[v.mediaId, v.providerId].join("|")} <WatchedMediaCard
media={v} key={[v.mediaId, v.providerId].join("|")}
series media={v}
/> series
))} />
))}
</MediaGrid>
</SectionHeading> </SectionHeading>
); );
} }

View file

@ -5,7 +5,7 @@ export function SearchLoadingView() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Loading <Loading
className="my-24" className="mt-40"
text={t("search.loading") || "Fetching your favourite shows..."} text={t("search.loading") || "Fetching your favourite shows..."}
/> />
); );

View file

@ -1,6 +1,7 @@
import { IconPatch } from "@/components/buttons/IconPatch"; import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading"; import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers";
@ -19,7 +20,7 @@ function SearchSuffix(props: {
const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH;
return ( return (
<div className="my-24 flex flex-col items-center justify-center space-y-3 text-center"> <div className="mt-40 flex flex-col items-center justify-center space-y-3 text-center">
<IconPatch <IconPatch
icon={icon} icon={icon}
className={`text-xl ${allFailed ? "text-red-400" : "text-bink-600"}`} className={`text-xl ${allFailed ? "text-red-400" : "text-bink-600"}`}
@ -83,12 +84,14 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
title={t("search.headingTitle") || "Search results"} title={t("search.headingTitle") || "Search results"}
icon={Icons.SEARCH} icon={Icons.SEARCH}
> >
{results.results.map((v) => ( <MediaGrid>
<WatchedMediaCard {results.results.map((v) => (
key={[v.mediaId, v.providerId].join("|")} <WatchedMediaCard
media={v} key={[v.mediaId, v.providerId].join("|")}
/> media={v}
))} />
))}
</MediaGrid>
</SectionHeading> </SectionHeading>
) : null} ) : null}

View file

@ -5,8 +5,8 @@ import { SearchBarInput } from "@/components/SearchBar";
import Sticky from "react-stickynode"; import Sticky from "react-stickynode";
import { Title } from "@/components/text/Title"; import { Title } from "@/components/text/Title";
import { useSearchQuery } from "@/hooks/useSearchQuery"; import { useSearchQuery } from "@/hooks/useSearchQuery";
import { WideContainer } from "@/components/layout/WideContainer";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SearchResultsPartial } from "./SearchResultsPartial"; import { SearchResultsPartial } from "./SearchResultsPartial";
export function SearchView() { export function SearchView() {
@ -21,16 +21,16 @@ export function SearchView() {
return ( return (
<> <>
<div className="relative z-10"> <div className="relative z-10 mb-24">
<Navigation bg={showBg} /> <Navigation bg={showBg} />
<ThinContainer> <ThinContainer>
<div className="mt-44 space-y-16 text-center"> <div className="mt-44 space-y-16 text-center">
<div className="absolute left-0 bottom-0 right-0 flex h-0 justify-center"> <div className="absolute left-0 bottom-0 right-0 flex h-0 justify-center">
<div className="absolute bottom-4 h-[100vh] w-[300vh] rounded-[100%] bg-[#211D30]" /> <div className="absolute bottom-4 h-[100vh] w-[300vh] rounded-[100%] bg-denim-300" />
</div> </div>
<div className="relative z-20"> <div className="relative z-20">
<div className="mb-16"> <div className="mb-16">
<Title>{t("search.title")}</Title> <Title className="mx-auto max-w-xs">{t("search.title")}</Title>
</div> </div>
<Sticky enabled top={16} onStateChange={stickStateChanged}> <Sticky enabled top={16} onStateChange={stickStateChanged}>
<SearchBarInput <SearchBarInput
@ -46,9 +46,9 @@ export function SearchView() {
</div> </div>
</ThinContainer> </ThinContainer>
</div> </div>
<ThinContainer> <WideContainer>
<SearchResultsPartial search={search} /> <SearchResultsPartial search={search} />
</ThinContainer> </WideContainer>
</> </>
); );
} }

View file

@ -12,29 +12,29 @@ module.exports = {
"bink-500": "#8D66B5", "bink-500": "#8D66B5",
"bink-600": "#A87FD1", "bink-600": "#A87FD1",
"bink-700": "#CD97D6", "bink-700": "#CD97D6",
"denim-100": "#131119", "denim-100": "#120F1D",
"denim-200": "#1E1A29", "denim-200": "#191526",
"denim-300": "#282336", "denim-300": "#211D30",
"denim-400": "#322D43", "denim-400": "#2B263D",
"denim-500": "#433D55", "denim-500": "#38334A",
"denim-600": "#5A5370", "denim-600": "#504B64",
"denim-700": "#817998", "denim-700": "#7A758F"
}, },
/* fonts */ /* fonts */
fontFamily: { fontFamily: {
"open-sans": "'Open Sans'", "open-sans": "'Open Sans'"
}, },
/* animations */ /* animations */
keyframes: { keyframes: {
"loading-pin": { "loading-pin": {
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" }, "0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
"20%": { height: "1em", "background-color": "white" }, "20%": { height: "1em", "background-color": "white" }
}, }
}, },
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }, animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
}, }
}, },
plugins: [require("tailwind-scrollbar")], plugins: [require("tailwind-scrollbar"), require("@tailwindcss/line-clamp")]
}; };

View file

@ -254,6 +254,11 @@
"@swc/core-win32-ia32-msvc" "1.3.22" "@swc/core-win32-ia32-msvc" "1.3.22"
"@swc/core-win32-x64-msvc" "1.3.22" "@swc/core-win32-x64-msvc" "1.3.22"
"@tailwindcss/line-clamp@^0.4.2":
"integrity" "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw=="
"resolved" "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz"
"version" "0.4.2"
"@tootallnate/once@2": "@tootallnate/once@2":
"version" "2.0.0" "version" "2.0.0"
@ -1942,16 +1947,16 @@
"version" "1.1.4" "version" "1.1.4"
"json5@^1.0.1": "json5@^1.0.1":
"integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==" "integrity" "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="
"resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz"
"version" "1.0.1" "version" "1.0.2"
dependencies: dependencies:
"minimist" "^1.2.0" "minimist" "^1.2.0"
"json5@^2.2.0": "json5@^2.2.0":
"integrity" "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" "integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
"resolved" "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
"version" "2.2.1" "version" "2.2.3"
"jsonparse@^1.3.1": "jsonparse@^1.3.1":
"version" "1.3.1" "version" "1.3.1"
@ -3225,7 +3230,7 @@
"resolved" "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz" "resolved" "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz"
"version" "2.0.1" "version" "2.0.1"
"tailwindcss@^3.2.4", "tailwindcss@3.x": "tailwindcss@^3.2.4", "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@3.x":
"integrity" "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==" "integrity" "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ=="
"resolved" "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz" "resolved" "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz"
"version" "3.2.4" "version" "3.2.4"