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": {
"@tailwindcss/line-clamp": "^0.4.2",
"@types/crypto-js": "^4.1.1",
"@types/node": "^17.0.15",
"@types/react": "^17.0.39",

View File

@ -37,7 +37,7 @@ export function SearchBarInput(props: SearchBarProps) {
}
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">
<Icon icon={Icons.SEARCH} />
</div>

View File

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

View File

@ -12,9 +12,9 @@ export function IconPatch(props: IconPatchProps) {
return (
<div className={props.className || undefined} onClick={props.onClick}>
<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
? "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" : ""}`}
>

View File

@ -40,7 +40,7 @@ export function useBackdrop(): [
return [setBackdrop, backdropProps, highlightedProps];
}
export function Backdrop(props: BackdropProps) {
function Backdrop(props: BackdropProps) {
const clickEvent = props.onClick || (() => {});
const animationEvent = props.onBackdropHide || (() => {});
const [isVisible, setVisible, fadeProps] = useFade();
@ -59,7 +59,7 @@ export function Backdrop(props: BackdropProps) {
return (
<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" : ""
}`}
{...fadeProps}
@ -99,9 +99,9 @@ export function BackdropContainer(
return (
<div ref={root}>
{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} />
<div ref={copy} className="absolute">
<div ref={copy} className="pointer-events-auto absolute">
{props.children}
</div>
</div>,

View File

@ -6,13 +6,14 @@ export function BrandPill(props: { clickable?: boolean }) {
return (
<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
? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95"
className={`flex items-center space-x-2 rounded-full bg-bink-300 bg-opacity-50 px-4 py-2 text-bink-600 ${
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} />
<span className="font-semibold text-white">{t('global.name')}</span>
<span className="font-semibold text-white">{t("global.name")}</span>
</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,
MWMediaType,
} from "@/providers";
import { Icon, Icons } from "@/components/Icon";
import { serializePortableMedia } from "@/hooks/usePortableMedia";
import { DotList } from "@/components/text/DotList";
export interface MediaCardProps {
media: MWMediaMeta;
// eslint-disable-next-line react/no-unused-prop-types
watchedPercentage: number;
linkable?: boolean;
series?: boolean;
}
function MediaCardContent({
media,
linkable,
watchedPercentage,
series,
}: MediaCardProps) {
// TODO add progress back
function MediaCardContent({ media, series, linkable }: MediaCardProps) {
const provider = getProviderFromId(media.providerId);
if (!provider) {
@ -29,52 +26,31 @@ function MediaCardContent({
}
return (
<article
className={`group relative mb-4 flex overflow-hidden rounded bg-denim-300 py-4 px-5 ${
linkable ? "hover:bg-denim-400" : ""
<div
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
linkable ? "hover:bg-opacity-100" : ""
}`}
>
{/* progress background */}
{watchedPercentage > 0 ? (
<div className="absolute top-0 left-0 right-0 bottom-0">
<div
className="relative h-full bg-bink-300 bg-opacity-30"
style={{
width: `${watchedPercentage}%`,
}}
>
<div className="absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l from-bink-400 to-transparent opacity-40" />
</div>
</div>
) : null}
<div className="relative flex flex-1">
{/* card content */}
<div className="flex-1">
<h1 className="mb-1 font-bold text-white">
{media.title}
{series && media.seasonId && media.episodeId ? (
<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>
<article
className={`relative mb-2 p-3 transition-transform duration-100 ${
linkable ? "group-hover:scale-95" : ""
}`}
>
<div className="mb-4 aspect-[2/3] w-full rounded-xl bg-denim-500" />
<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 ? (
<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]}
/>
</article>
</div>
);
}

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 {
children?: React.ReactNode;
className?: string;
}
export function Title(props: TitleProps) {
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}
</h1>
);

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useLoading } from "@/hooks/useLoading";
import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers";
@ -19,7 +20,7 @@ function SearchSuffix(props: {
const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH;
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
icon={icon}
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"}
icon={Icons.SEARCH}
>
{results.results.map((v) => (
<WatchedMediaCard
key={[v.mediaId, v.providerId].join("|")}
media={v}
/>
))}
<MediaGrid>
{results.results.map((v) => (
<WatchedMediaCard
key={[v.mediaId, v.providerId].join("|")}
media={v}
/>
))}
</MediaGrid>
</SectionHeading>
) : null}

View File

@ -5,8 +5,8 @@ import { SearchBarInput } from "@/components/SearchBar";
import Sticky from "react-stickynode";
import { Title } from "@/components/text/Title";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import { WideContainer } from "@/components/layout/WideContainer";
import { useTranslation } from "react-i18next";
import { SearchResultsPartial } from "./SearchResultsPartial";
export function SearchView() {
@ -21,16 +21,16 @@ export function SearchView() {
return (
<>
<div className="relative z-10">
<div className="relative z-10 mb-24">
<Navigation bg={showBg} />
<ThinContainer>
<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 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 className="relative z-20">
<div className="mb-16">
<Title>{t("search.title")}</Title>
<Title className="mx-auto max-w-xs">{t("search.title")}</Title>
</div>
<Sticky enabled top={16} onStateChange={stickStateChanged}>
<SearchBarInput
@ -46,9 +46,9 @@ export function SearchView() {
</div>
</ThinContainer>
</div>
<ThinContainer>
<WideContainer>
<SearchResultsPartial search={search} />
</ThinContainer>
</WideContainer>
</>
);
}

View File

@ -12,29 +12,29 @@ module.exports = {
"bink-500": "#8D66B5",
"bink-600": "#A87FD1",
"bink-700": "#CD97D6",
"denim-100": "#131119",
"denim-200": "#1E1A29",
"denim-300": "#282336",
"denim-400": "#322D43",
"denim-500": "#433D55",
"denim-600": "#5A5370",
"denim-700": "#817998",
"denim-100": "#120F1D",
"denim-200": "#191526",
"denim-300": "#211D30",
"denim-400": "#2B263D",
"denim-500": "#38334A",
"denim-600": "#504B64",
"denim-700": "#7A758F"
},
/* fonts */
fontFamily: {
"open-sans": "'Open Sans'",
"open-sans": "'Open Sans'"
},
/* animations */
keyframes: {
"loading-pin": {
"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-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":
"version" "2.0.0"
@ -1942,16 +1947,16 @@
"version" "1.1.4"
"json5@^1.0.1":
"integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow=="
"resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz"
"version" "1.0.1"
"integrity" "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="
"resolved" "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz"
"version" "1.0.2"
dependencies:
"minimist" "^1.2.0"
"json5@^2.2.0":
"integrity" "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
"resolved" "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz"
"version" "2.2.1"
"integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
"resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
"version" "2.2.3"
"jsonparse@^1.3.1":
"version" "1.3.1"
@ -3225,7 +3230,7 @@
"resolved" "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz"
"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=="
"resolved" "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz"
"version" "3.2.4"