Reorganize views folder

This commit is contained in:
mrjvs 2023-08-20 18:45:07 +02:00
parent 1fde44076a
commit ec3b96a399
22 changed files with 354 additions and 467 deletions

View File

@ -1,4 +1,4 @@
import { ReactNode, useState } from "react"; import { ReactNode } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { IconPatch } from "@/components/buttons/IconPatch"; import { IconPatch } from "@/components/buttons/IconPatch";
@ -6,7 +6,6 @@ import { Icons } from "@/components/Icon";
import { Lightbar } from "@/components/utils/Lightbar"; import { Lightbar } from "@/components/utils/Lightbar";
import { useBannerSize } from "@/hooks/useBanner"; import { useBannerSize } from "@/hooks/useBanner";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import SettingsModal from "@/views/SettingsModal";
import { BrandPill } from "./BrandPill"; import { BrandPill } from "./BrandPill";
@ -17,7 +16,6 @@ export interface NavigationProps {
export function Navigation(props: NavigationProps) { export function Navigation(props: NavigationProps) {
const bannerHeight = useBannerSize(); const bannerHeight = useBannerSize();
const [showModal, setShowModal] = useState(false);
return ( return (
<> <>
<div className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center"> <div className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center">
@ -52,14 +50,6 @@ export function Navigation(props: NavigationProps) {
props.children ? "hidden sm:flex" : "flex" props.children ? "hidden sm:flex" : "flex"
} relative flex-row gap-4`} } relative flex-row gap-4`}
> >
<IconPatch
className="text-2xl text-white"
icon={Icons.GEAR}
clickable
onClick={() => {
setShowModal(true);
}}
/>
<a <a
href={conf().DISCORD_LINK} href={conf().DISCORD_LINK}
target="_blank" target="_blank"
@ -78,7 +68,6 @@ export function Navigation(props: NavigationProps) {
</a> </a>
</div> </div>
</div> </div>
<SettingsModal show={showModal} onClose={() => setShowModal(false)} />
</div> </div>
</> </>
); );

View File

@ -15,9 +15,9 @@ import { Layout } from "@/setup/Layout";
import { BookmarkContextProvider } from "@/state/bookmark"; import { BookmarkContextProvider } from "@/state/bookmark";
import { SettingsProvider } from "@/state/settings"; import { SettingsProvider } from "@/state/settings";
import { WatchedContextProvider } from "@/state/watched"; import { WatchedContextProvider } from "@/state/watched";
import { NotFoundPage } from "@/views/errors/NotFoundPage";
import { HomePage } from "@/views/HomePage";
import { MediaView } from "@/views/media/MediaView"; import { MediaView } from "@/views/media/MediaView";
import { NotFoundPage } from "@/views/notfound/NotFoundView";
import { SearchView } from "@/views/search/SearchView";
function LegacyUrlView({ children }: { children: ReactElement }) { function LegacyUrlView({ children }: { children: ReactElement }) {
const location = useLocation(); const location = useLocation();
@ -85,16 +85,14 @@ function App() {
<Route <Route
exact exact
path={["/browse/:query?", "/"]} path={["/browse/:query?", "/"]}
component={SearchView} component={HomePage}
/> />
{/* other */} {/* other */}
<Route <Route
exact exact
path="/dev" path="/dev"
component={lazy( component={lazy(() => import("@/views/DeveloperPage"))}
() => import("@/views/developer/DeveloperView")
)}
/> />
<Route <Route
exact exact

View File

@ -3,7 +3,7 @@ import { ThinContainer } from "@/components/layout/ThinContainer";
import { ArrowLink } from "@/components/text/ArrowLink"; import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title"; import { Title } from "@/components/text/Title";
export default function DeveloperView() { export default function DeveloperPage() {
return ( return (
<div className="py-48"> <div className="py-48">
<Navigation /> <Navigation />

View File

@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { MWQuery } from "@/backend/metadata/types/mw";
import { WideContainer } from "@/components/layout/WideContainer";
import { useDebounce } from "@/hooks/useDebounce";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import { HomeLayout } from "@/views/layouts/HomeLayout";
import { BookmarksPart } from "@/views/parts/home/BookmarksPart";
import { HeroPart } from "@/views/parts/home/HeroPart";
import { WatchingPart } from "@/views/parts/home/WatchingPart";
import { SearchListPart } from "@/views/parts/search/SearchListPart";
import { SearchLoadingPart } from "@/views/parts/search/SearchLoadingPart";
function useSearch(search: MWQuery) {
const [searching, setSearching] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const debouncedSearch = useDebounce<MWQuery>(search, 500);
useEffect(() => {
setSearching(search.searchQuery !== "");
setLoading(search.searchQuery !== "");
}, [search]);
useEffect(() => {
setLoading(false);
}, [debouncedSearch]);
return {
loading,
searching,
};
}
export function HomePage() {
const { t } = useTranslation();
const [showBg, setShowBg] = useState<boolean>(false);
const searchParams = useSearchQuery();
const [search] = searchParams;
const s = useSearch(search);
return (
<HomeLayout showBg={showBg}>
<div className="relative z-10 mb-16 sm:mb-24">
<Helmet>
<title>{t("global.name")}</title>
</Helmet>
<HeroPart searchParams={searchParams} setIsSticky={setShowBg} />
</div>
<WideContainer>
{s.loading ? (
<SearchLoadingPart />
) : s.searching ? (
<SearchListPart searchQuery={search} />
) : (
<>
<BookmarksPart />
<WatchingPart />
</>
)}
</WideContainer>
</HomeLayout>
);
}

View File

@ -9,8 +9,7 @@ import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid"; 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 { SearchLoadingPart } from "@/views/parts/search/SearchLoadingPart";
import { SearchLoadingView } from "./SearchLoadingView";
function SearchSuffix(props: { failed?: boolean; results?: number }) { function SearchSuffix(props: { failed?: boolean; results?: number }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -63,7 +62,7 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
if (searchQuery.searchQuery !== "") runSearch(searchQuery); if (searchQuery.searchQuery !== "") runSearch(searchQuery);
}, [searchQuery, runSearchQuery]); }, [searchQuery, runSearchQuery]);
if (loading) return <SearchLoadingView />; if (loading) return <SearchLoadingPart />;
if (error) return <SearchSuffix failed />; if (error) return <SearchSuffix failed />;
if (!results) return null; if (!results) return null;

View File

@ -1,148 +0,0 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import CaptionColorSelector, {
colors,
} from "@/components/CaptionColorSelector";
import { Dropdown } from "@/components/Dropdown";
import { Icon, Icons } from "@/components/Icon";
import { Modal, ModalCard } from "@/components/layout/Modal";
import { Slider } from "@/components/Slider";
import { conf } from "@/setup/config";
import { appLanguageOptions } from "@/setup/i18n";
import {
CaptionLanguageOption,
LangCode,
captionLanguages,
} from "@/setup/iso6391";
import { useSettings } from "@/state/settings";
import { CaptionCue } from "@/video/components/actions/CaptionRendererAction";
export default function SettingsModal(props: {
onClose: () => void;
show: boolean;
}) {
const {
captionSettings,
language,
setLanguage,
setCaptionLanguage,
setCaptionBackgroundColor,
setCaptionFontSize,
} = useSettings();
const { t, i18n } = useTranslation();
const selectedCaptionLanguage = useMemo(
() => captionLanguages.find((l) => l.id === captionSettings.language),
[captionSettings.language]
) as CaptionLanguageOption;
const appLanguage = useMemo(
() => appLanguageOptions.find((l) => l.id === language),
[language]
) as CaptionLanguageOption;
const captionBackgroundOpacity = (
(parseInt(captionSettings.style.backgroundColor.substring(7, 9), 16) /
255) *
100
).toFixed(0);
return (
<Modal show={props.show}>
<ModalCard className="text-white">
<div className="flex flex-col gap-4">
<div className="flex flex-row justify-between">
<span className="text-xl font-bold">{t("settings.title")}</span>
<div
onClick={() => props.onClose()}
className="hover:cursor-pointer"
>
<Icon icon={Icons.X} />
</div>
</div>
<div className="flex flex-col gap-10 lg:flex-row">
<div className="lg:w-1/2">
<div className="flex flex-col justify-between">
<label className="text-md font-semibold">
{t("settings.language")}
</label>
<Dropdown
selectedItem={appLanguage}
setSelectedItem={(val) => {
i18n.changeLanguage(val.id);
setLanguage(val.id as LangCode);
}}
options={appLanguageOptions}
/>
</div>
<div className="flex flex-col justify-between">
<label className="text-md font-semibold">
{t("settings.captionLanguage")}
</label>
<Dropdown
selectedItem={selectedCaptionLanguage}
setSelectedItem={(val) => {
setCaptionLanguage(val.id as LangCode);
}}
options={captionLanguages}
/>
</div>
<div className="flex flex-col justify-between">
<Slider
label={
t(
"videoPlayer.popouts.captionPreferences.fontSize"
) as string
}
min={14}
step={1}
max={60}
value={captionSettings.style.fontSize}
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
/>
<Slider
label={
t(
"videoPlayer.popouts.captionPreferences.opacity"
) as string
}
step={1}
min={0}
max={255}
valueDisplay={`${captionBackgroundOpacity}%`}
value={parseInt(
captionSettings.style.backgroundColor.substring(7, 9),
16
)}
onChange={(e) =>
setCaptionBackgroundColor(e.target.valueAsNumber)
}
/>
<div className="flex flex-row justify-between">
<label className="font-bold" htmlFor="color">
{t("videoPlayer.popouts.captionPreferences.color")}
</label>
<div className="flex flex-row gap-2">
{colors.map((color) => (
<CaptionColorSelector key={color} color={color} />
))}
</div>
</div>
</div>
<div />
</div>
<div className="flex w-full flex-col justify-center">
<div className="flex aspect-video flex-col justify-end rounded bg-zinc-800">
<div className="pointer-events-none flex w-full flex-col items-center transition-[bottom]">
<CaptionCue
scale={0.5}
text={selectedCaptionLanguage.nativeName}
/>
</div>
</div>
</div>
</div>
</div>
<div className="float-right mt-1 text-sm">v{conf().APP_VERSION}</div>
</ModalCard>
</Modal>
);
}

View File

@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
import { ErrorWrapperPart } from "@/views/parts/errors/ErrorWrapperPart";
export function NotFoundPage() {
const { t } = useTranslation();
return (
<ErrorWrapperPart>
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.page.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">{t("notFound.page.description")}</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</ErrorWrapperPart>
);
}

View File

@ -0,0 +1,14 @@
import { FooterView } from "@/components/layout/Footer";
import { Navigation } from "@/components/layout/Navigation";
export function HomeLayout(props: {
showBg: boolean;
children: React.ReactNode;
}) {
return (
<FooterView>
<Navigation bg={props.showBg} />
{props.children}
</FooterView>
);
}

View File

@ -0,0 +1,11 @@
import { FooterView } from "@/components/layout/Footer";
import { Navigation } from "@/components/layout/Navigation";
export function PageLayout(props: { children: React.ReactNode }) {
return (
<FooterView>
<Navigation />
{props.children}
</FooterView>
);
}

View File

@ -24,10 +24,11 @@ import { SourceController } from "@/video/components/controllers/SourceControlle
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader"; import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
import { VideoPlayer } from "@/video/components/VideoPlayer"; import { VideoPlayer } from "@/video/components/VideoPlayer";
import { VideoPlayerMeta } from "@/video/state/types"; import { VideoPlayerMeta } from "@/video/state/types";
import { ErrorWrapperPart } from "@/views/parts/errors/ErrorWrapperPart";
import { MediaNotFoundPart } from "@/views/parts/errors/MediaNotFoundPart";
import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaFetchErrorView } from "./MediaErrorView";
import { MediaScrapeLog } from "./MediaScrapeLog"; import { MediaScrapeLog } from "./MediaScrapeLog";
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
function MediaViewLoading(props: { onGoBack(): void }) { function MediaViewLoading(props: { onGoBack(): void }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -241,9 +242,9 @@ export function MediaView() {
if (error) return <MediaFetchErrorView />; if (error) return <MediaFetchErrorView />;
if (!meta || !selected) if (!meta || !selected)
return ( return (
<NotFoundWrapper video> <ErrorWrapperPart video>
<NotFoundMedia /> <MediaNotFoundPart />
</NotFoundWrapper> </ErrorWrapperPart>
); );
// scraping view will start scraping and return with onStream // scraping view will start scraping and return with onStream

View File

@ -1,87 +0,0 @@
import { ReactNode } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Navigation } from "@/components/layout/Navigation";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
import { useGoBack } from "@/hooks/useGoBack";
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
export function NotFoundWrapper(props: {
children?: ReactNode;
video?: boolean;
}) {
const { t } = useTranslation();
const goBack = useGoBack();
return (
<div className="relative flex flex-1 flex-col">
<Helmet>
<title>{t("notFound.genericTitle")}</title>
</Helmet>
{props.video ? (
<div className="absolute inset-x-0 top-0 px-8 py-6">
<VideoPlayerHeader onClick={goBack} />
</div>
) : (
<Navigation />
)}
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
{props.children}
</div>
</div>
);
}
export function NotFoundMedia() {
const { t } = useTranslation();
return (
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.media.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">{t("notFound.media.description")}</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</div>
);
}
export function NotFoundProvider() {
const { t } = useTranslation();
return (
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.provider.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">
{t("notFound.provider.description")}
</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</div>
);
}
export function NotFoundPage() {
const { t } = useTranslation();
return (
<NotFoundWrapper>
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.page.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">{t("notFound.page.description")}</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</NotFoundWrapper>
);
}

View File

@ -0,0 +1,33 @@
import { ReactNode } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Navigation } from "@/components/layout/Navigation";
import { useGoBack } from "@/hooks/useGoBack";
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
export function ErrorWrapperPart(props: {
children?: ReactNode;
video?: boolean;
}) {
const { t } = useTranslation();
const goBack = useGoBack();
return (
<div className="relative flex flex-1 flex-col">
<Helmet>
<title>{t("notFound.genericTitle")}</title>
</Helmet>
{props.video ? (
<div className="absolute inset-x-0 top-0 px-8 py-6">
<VideoPlayerHeader onClick={goBack} />
</div>
) : (
<Navigation />
)}
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
{props.children}
</div>
</div>
);
}

View File

@ -0,0 +1,22 @@
import { useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
export function MediaNotFoundPart() {
const { t } = useTranslation();
return (
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.media.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">{t("notFound.media.description")}</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</div>
);
}

View File

@ -0,0 +1,24 @@
import { useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
export function ProviderNotFoundPart() {
const { t } = useTranslation();
return (
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
<IconPatch
icon={Icons.EYE_SLASH}
className="mb-6 text-xl text-bink-600"
/>
<Title>{t("notFound.provider.title")}</Title>
<p className="mb-12 mt-5 max-w-sm">
{t("notFound.provider.description")}
</p>
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
</div>
);
}

View File

@ -0,0 +1,55 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import Sticky from "react-stickynode";
import { ThinContainer } from "@/components/layout/ThinContainer";
import { SearchBarInput } from "@/components/SearchBar";
import { Title } from "@/components/text/Title";
import { useBannerSize } from "@/hooks/useBanner";
import { useSearchQuery } from "@/hooks/useSearchQuery";
export interface HeroPartProps {
setIsSticky: (val: boolean) => void;
searchParams: ReturnType<typeof useSearchQuery>;
}
export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
const { t } = useTranslation();
const [search, setSearch, setSearchUnFocus] = searchParams;
const [, setShowBg] = useState(false);
const bannerSize = useBannerSize();
const stickStateChanged = useCallback(
({ status }: Sticky.Status) => {
const val = status === Sticky.STATUS_FIXED;
setShowBg(val);
setIsSticky(val);
},
[setShowBg, setIsSticky]
);
return (
<ThinContainer>
<div className="mt-44 space-y-16 text-center">
<div className="relative z-10 mb-16">
<Title className="mx-auto max-w-xs">{t("search.title")}</Title>
</div>
<div className="relative z-30">
<Sticky
enabled
top={16 + bannerSize}
onStateChange={stickStateChanged}
>
<SearchBarInput
onChange={setSearch}
value={search}
onUnFocus={setSearchUnFocus}
placeholder={
t("search.placeholder") || "What do you want to watch?"
}
/>
</Sticky>
</div>
</div>
</ThinContainer>
);
}

View File

@ -2,13 +2,18 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { EditButton } from "@/components/buttons/EditButton";
import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { import {
getIfBookmarkedFromPortable, getIfBookmarkedFromPortable,
useBookmarkContext, useBookmarkContext,
} from "@/state/bookmark"; } from "@/state/bookmark";
import { useWatchedContext } from "@/state/watched"; import { useWatchedContext } from "@/state/watched";
function Watched() { export function WatchingPart() {
const { t } = useTranslation(); const { t } = useTranslation();
const { getFilteredBookmarks } = useBookmarkContext(); const { getFilteredBookmarks } = useBookmarkContext();
const { getFilteredWatched, removeProgress } = useWatchedContext(); const { getFilteredWatched, removeProgress } = useWatchedContext();

View File

@ -0,0 +1,88 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { searchForMedia } from "@/backend/metadata/search";
import { MWMediaMeta, MWQuery } from "@/backend/metadata/types/mw";
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 { SearchLoadingPart } from "@/views/parts/search/SearchLoadingPart";
function SearchSuffix(props: { failed?: boolean; results?: number }) {
const { t } = useTranslation();
const icon: Icons = props.failed ? Icons.WARNING : Icons.EYE_SLASH;
return (
<div className="mb-24 mt-40 flex flex-col items-center justify-center space-y-3 text-center">
<IconPatch
icon={icon}
className={`text-xl ${props.failed ? "text-red-400" : "text-bink-600"}`}
/>
{/* standard suffix */}
{!props.failed ? (
<div>
{(props.results ?? 0) > 0 ? (
<p>{t("search.allResults")}</p>
) : (
<p>{t("search.noResults")}</p>
)}
</div>
) : null}
{/* Error result */}
{props.failed ? (
<div>
<p>{t("search.allFailed")}</p>
</div>
) : null}
</div>
);
}
export function SearchListPart({ searchQuery }: { searchQuery: MWQuery }) {
const { t } = useTranslation();
const [results, setResults] = useState<MWMediaMeta[]>([]);
const [runSearchQuery, loading, error] = useLoading((query: MWQuery) =>
searchForMedia(query)
);
useEffect(() => {
async function runSearch(query: MWQuery) {
const searchResults = await runSearchQuery(query);
if (!searchResults) return;
setResults(searchResults);
}
if (searchQuery.searchQuery !== "") runSearch(searchQuery);
}, [searchQuery, runSearchQuery]);
if (loading) return <SearchLoadingPart />;
if (error) return <SearchSuffix failed />;
if (!results) return null;
return (
<div>
{results.length > 0 ? (
<div>
<SectionHeading
title={t("search.headingTitle") || "Search results"}
icon={Icons.SEARCH}
/>
<MediaGrid>
{results.map((v) => (
<WatchedMediaCard key={v.id.toString()} media={v} />
))}
</MediaGrid>
</div>
) : null}
<SearchSuffix results={results.length} />
</div>
);
}

View File

@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
import { Loading } from "@/components/layout/Loading"; import { Loading } from "@/components/layout/Loading";
export function SearchLoadingView() { export function SearchLoadingPart() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Loading className="mb-24 mt-40 " text={t("search.loading") || "..."} /> <Loading className="mb-24 mt-40 " text={t("search.loading") || "..."} />

View File

@ -1,106 +0,0 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { EditButton } from "@/components/buttons/EditButton";
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,
useBookmarkContext,
} from "@/state/bookmark";
import { useWatchedContext } from "@/state/watched";
function Bookmarks() {
const { t } = useTranslation();
const { getFilteredBookmarks, setItemBookmark } = useBookmarkContext();
const bookmarks = getFilteredBookmarks();
const [editing, setEditing] = useState(false);
const [gridRef] = useAutoAnimate<HTMLDivElement>();
const { watched } = useWatchedContext();
const bookmarksSorted = useMemo(() => {
return bookmarks
.map((v) => {
return {
...v,
watched: watched.items
.sort((a, b) => b.watchedAt - a.watchedAt)
.find((watchedItem) => watchedItem.item.meta.id === v.id),
};
})
.sort(
(a, b) => (b.watched?.watchedAt || 0) - (a.watched?.watchedAt || 0)
);
}, [watched.items, bookmarks]);
if (bookmarks.length === 0) return null;
return (
<div>
<SectionHeading
title={t("search.bookmarks") || "Bookmarks"}
icon={Icons.BOOKMARK}
>
<EditButton editing={editing} onEdit={setEditing} />
</SectionHeading>
<MediaGrid ref={gridRef}>
{bookmarksSorted.map((v) => (
<WatchedMediaCard
key={v.id}
media={v}
closable={editing}
onClose={() => setItemBookmark(v, false)}
/>
))}
</MediaGrid>
</div>
);
}
function Watched() {
const { t } = useTranslation();
const { getFilteredBookmarks } = useBookmarkContext();
const { getFilteredWatched, removeProgress } = useWatchedContext();
const [editing, setEditing] = useState(false);
const [gridRef] = useAutoAnimate<HTMLDivElement>();
const bookmarks = getFilteredBookmarks();
const watchedItems = getFilteredWatched().filter(
(v) => !getIfBookmarkedFromPortable(bookmarks, v.item.meta)
);
if (watchedItems.length === 0) return null;
return (
<div>
<SectionHeading
title={t("search.continueWatching") || "Continue Watching"}
icon={Icons.CLOCK}
>
<EditButton editing={editing} onEdit={setEditing} />
</SectionHeading>
<MediaGrid ref={gridRef}>
{watchedItems.map((v) => (
<WatchedMediaCard
key={v.item.meta.id}
media={v.item.meta}
closable={editing}
onClose={() => removeProgress(v.item.meta.id)}
/>
))}
</MediaGrid>
</div>
);
}
export function HomeView() {
return (
<div>
<Bookmarks />
<Watched />
</div>
);
}

View File

@ -1,34 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import { MWQuery } from "@/backend/metadata/types/mw";
import { useDebounce } from "@/hooks/useDebounce";
import { HomeView } from "./HomeView";
import { SearchLoadingView } from "./SearchLoadingView";
import { SearchResultsView } from "./SearchResultsView";
interface SearchResultsPartialProps {
search: MWQuery;
}
export function SearchResultsPartial({ search }: SearchResultsPartialProps) {
const [searching, setSearching] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const debouncedSearch = useDebounce<MWQuery>(search, 500);
useEffect(() => {
setSearching(search.searchQuery !== "");
setLoading(search.searchQuery !== "");
}, [search]);
useEffect(() => {
setLoading(false);
}, [debouncedSearch]);
const resultView = useMemo(() => {
if (loading) return <SearchLoadingView />;
if (searching) return <SearchResultsView searchQuery={debouncedSearch} />;
return <HomeView />;
}, [loading, searching, debouncedSearch]);
return resultView;
}

View File

@ -1,64 +0,0 @@
import { useCallback, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import Sticky from "react-stickynode";
import { FooterView } from "@/components/layout/Footer";
import { Navigation } from "@/components/layout/Navigation";
import { ThinContainer } from "@/components/layout/ThinContainer";
import { WideContainer } from "@/components/layout/WideContainer";
import { SearchBarInput } from "@/components/SearchBar";
import { Title } from "@/components/text/Title";
import { useBannerSize } from "@/hooks/useBanner";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import { SearchResultsPartial } from "./SearchResultsPartial";
export function SearchView() {
const { t } = useTranslation();
const [search, setSearch, setSearchUnFocus] = useSearchQuery();
const [showBg, setShowBg] = useState(false);
const bannerSize = useBannerSize();
const stickStateChanged = useCallback(
({ status }: Sticky.Status) => setShowBg(status === Sticky.STATUS_FIXED),
[setShowBg]
);
return (
<FooterView>
<Navigation bg={showBg} />
<div className="relative z-10 mb-16 sm:mb-24">
<Helmet>
<title>{t("global.name")}</title>
</Helmet>
<ThinContainer>
<div className="mt-44 space-y-16 text-center">
<div className="relative z-10 mb-16">
<Title className="mx-auto max-w-xs">{t("search.title")}</Title>
</div>
<div className="relative z-30">
<Sticky
enabled
top={16 + bannerSize}
onStateChange={stickStateChanged}
>
<SearchBarInput
onChange={setSearch}
value={search}
onUnFocus={setSearchUnFocus}
placeholder={
t("search.placeholder") || "What do you want to watch?"
}
/>
</Sticky>
</div>
</div>
</ThinContainer>
</div>
<WideContainer>
<SearchResultsPartial search={search} />
</WideContainer>
</FooterView>
);
}