mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-30 16:17:41 +01:00
Reorganize views folder
This commit is contained in:
parent
1fde44076a
commit
ec3b96a399
22 changed files with 354 additions and 467 deletions
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 />
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
23
src/views/errors/NotFoundPage.tsx
Normal file
23
src/views/errors/NotFoundPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
14
src/views/layouts/HomeLayout.tsx
Normal file
14
src/views/layouts/HomeLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
11
src/views/layouts/PageLayout.tsx
Normal file
11
src/views/layouts/PageLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
33
src/views/parts/errors/ErrorWrapperPart.tsx
Normal file
33
src/views/parts/errors/ErrorWrapperPart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
22
src/views/parts/errors/MediaNotFoundPart.tsx
Normal file
22
src/views/parts/errors/MediaNotFoundPart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
24
src/views/parts/errors/ProviderNotFoundPart.tsx
Normal file
24
src/views/parts/errors/ProviderNotFoundPart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
55
src/views/parts/home/HeroPart.tsx
Normal file
55
src/views/parts/home/HeroPart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
88
src/views/parts/search/SearchListPart.tsx
Normal file
88
src/views/parts/search/SearchListPart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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") || "..."} />
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Reference in a new issue